# Arena by Dravensoft, the Angular corpus > Use this skill to build user interfaces with Arena, a token-driven design system with React and Angular component libraries on a shared Tailwind layer, for production screens or for throwaway prototypes and mocks. Covers design tokens, colour, type, spacing, motion, iconography, and the accessibility pattern each component binds. Arena carries the design language and not the skin: it ships Dravensoft's palette and fonts, and any project declares its own in arena.config.json and answers the kernel's style roles with a style plugin of its own. Every document Arena hands a builder working in Angular, concatenated in the order the router reads them. Nothing from the other framework layer is here, because a component ships under both names and the two documents are not interchangeable. --- name: design license: MIT description: "Use this skill to build user interfaces with Arena, a token-driven design system with React and Angular component libraries on a shared Tailwind layer, for production screens or for throwaway prototypes and mocks. Covers design tokens, colour, type, spacing, motion, iconography, and the accessibility pattern each component binds. Arena carries the design language and not the skin: it ships Dravensoft's palette and fonts, and any project declares its own in arena.config.json and answers the kernel's style roles with a style plugin of its own." metadata: homepage: https://arena.dravensoft.org --- # Arena Arena is Dravensoft's design language: a token layer, React and Angular component libraries built on it, and a shared Tailwind layer. Its identity is warm black under bone text, crimson as the voice and gold as distinction. **Arena is a product-application library**: tables, forms, navigation, dialogs, charts, cards and the states around them, the furniture of an application somebody works in. A media or a consumer product is a different register, so a photo wall, a feed of posts, a document editor or a game map is markup you write yourself; the skin travels either way, because a style plugin answers every role whatever the product is. **When that is your product**, or one screen of it, [`references/media-register.md`](./references/media-register.md) is what Arena hands you instead of a component: the pattern your markup binds, the parts the package already ships for it, and the one rule that bends. **This file routes. Read only what your task needs.** The rules below bind the code you write; the table under them routes. Read the rules once rather than per screen. ## Which job is this? **Building something with Arena** (a screen, a prototype, a skin, an integration): stay here, and follow the table below. **On a project you have not walked yet**, start at [`references/cold-start.md`](./references/cold-start.md) instead, whose first question is whether the project already carries Arena: one that does is a short walk that reads the config and the plugin it already has, and one that does not is the only page asking what this product looks like before anything answers. **Changing Arena itself** (adding a component, moving a token, editing a contract or a gate): read [`AGENTS.md`](https://github.com/dravensoft-dev/arena/blob/main/AGENTS.md) instead. It is the root of that branch and this file is not, and it is named by URL because that branch is the repository's and reaches nothing a package or this site carries. **A value you answer is not a value you move**, which is what decides the branch when the job is an appearance: a role your project fills is this branch's whatever it paints, and a scale step every product reads is the other one's, the type and page rhythm ladders aside. **Every path here outside `references/` is a repository path**, carried by a clone, by the plugin, and by the copy `arena-to-prod --skill` installs; read on the repository above otherwise. **Everything here is one component at a time.** ## In this order **Before the first screen, and once per project rather than per screen**, settle what this product is and what it looks like, because a screen written ahead of that is written against decisions nobody made. [`references/cold-start.md`](./references/cold-start.md) is where that happens, and it is a tree rather than a list, whose branches reach the four decisions a project settles once, the interview that comes before them when there is no appearance to read, the catalogue of measured style plugins a project can start from instead, the identity document every branch converges on before a file is written, and the two questions that decide an install rather than a screen: how much of the product has to be found from outside it, and what it is assembled on. Its last node derives the dependency list from those answers, so nothing has to be guessed at install time. Then, per screen: 1. **`frameworks//INDEX.md`**: the directory of your framework's components, naming every one under the category it is filed under. Read your layer's, and no other. It is short because it describes nothing: it tells you which category holds the name you are reaching for. 2. **`frameworks//components//INDEX.md`**, linked from there: every component in that category under the names the layer binds them to, with what each one is and what it takes, each linking its own prompt. Read the categories you are reaching into, and no others: a screen of any size reaches into two or three, and the saving is the four you skip. 3. **The component's own `.prompt.md`**, linked from that index: its members as a table, its examples and its Do/Don't. Read one per component you actually write, and no more. [`frameworks/INDEX.md`](../../frameworks/INDEX.md) is the layer-neutral index beside those three, and it answers the one question your layer's cannot, which the table below routes. Skip it when you already know what you are reaching for. A prompt states every member's type and default, so `contracts/api/components/.json` is only for the reasoning behind one, and you will rarely need it. ## The rules, and they are not style preferences Every one of these is a rule of the language rather than a preference, and most are enforced inside Arena by a gate over Arena's own tree. **No gate reads your application**, so in your code these hold because you hold them. What comes closest is `arena-to-prod --audit`, which reads your sources for the five of these that source text can show: a class of your own on an Arena component, one wrapped in your router's link, a raw value where a token belongs, an icon as an element, an emoji. It reports rather than fails unless you add `--strict`, and it decides nothing about the rest, so breaking one of those is still a defect nothing will report. - **Tokens are the only styling layer.** A raw colour, or a bare `16px`, is a bug. A hex, a channel triple in `rgb()` or `oklch()`, and a colour's own name are the same defect written three ways. Read a value through its custom property (`var(--crimson)`, `var(--sp-4)`), derive it with `calc()`/`clamp()` over one, or mix it with `color-mix()` over one. - **Danger is outline, never filled**: transparent background, border and content in `--error`/`--danger`. The single filled danger surface in the whole system is the final irreversible confirmation inside `ArenaConfirmDialog`. - **One primary accent per view.** Crimson is the voice; at most one `variant="primary"` action on a screen. Gold is distinction and focus, not a second primary. - **No gradients** on any surface. Depth comes from the `base-100` to `base-200` to `base-300` surface scale, the hairline border and the warm shadow. `ArenaSkeleton`'s neutral shimmer is the one exception. - **No emoji**, in product or in copy. - **Icons are Phosphor class-name strings, never elements and never SVG**: `icon="ph-bold ph-plus"`. Install `@phosphor-icons/web`; Arena never bundles it. - **Two themes, dark first.** Dark is `:root`, light is the `.arena-light` class. Components are never rewritten per theme, because they read tokens. `.arena-compact` re-densifies and `.arena-comfortable` grows the controls to a 48px touch target; the two are exclusive. - **A chart carries identity or meaning, never both.** The `--color-cat-*` ramp in fixed order is identity; the status colours are meaning. Status colours are never series colours. - **Copy is English, formal and direct**, concrete action verbs, no boastful adjectives. Errors are blame-free and say what to do next. - **An anchor Arena draws splits its activations.** A primary click with no modifier, and Enter, are cancelled and reported through the component's own event, so route from that handler and nothing navigates twice. A modified click, a middle click and the context menu are the browser's: they open the `href` themselves and report nothing. **Never wrap an Arena component in your router's own link**, which nests an anchor inside an anchor, and in Angular does not bind at all. `ArenaCard.href`, `ArenaCommand.route`, `ArenaCrumb.href` and `ArenaSideNavItem.href`. - **A press that starts on a control keeps to that control.** Where Arena draws an activation target around content you write, a card or a table row, a click or an Enter that begins on a button, a link or a field inside it runs that control and nothing else; a press anywhere else on the surface activates the surface. So a card or a row may hold your own controls, and it may also hand the press over entirely by not being interactive at all. - **A required member absent is a caller bug**, not a state to render. Every layer fails hard rather than drawing something empty, so an absent one is loud on the first render. - **No render follows from whether you bound a listener or filled a slot.** A member decides, always, because at least one platform cannot ask the question. - **A few components answer with a method rather than a member**, since no member is imperative. The component's own document names them where they exist. ## Where each question is answered | Question | Read | |---|---| | I am starting a project that has no appearance yet. What do I ask, and in what order? | [`references/cold-start.md`](./references/cold-start.md): the tree, its five questions, the catalogue of measured style plugins, and the identity document every branch converges on | | Which layer, which architecture, and what do I have to install? | the same tree's last three nodes: how much the product has to be found decides the layer and a peer, what it is assembled on is answered per architecture with the evidence for each, and the dependency list follows from both | | What may I build this with, and how sure is Arena about each answer? | [`references/stack.md`](./references/stack.md): the package manager, the runner, the module format, the bundler and the framework version, each answer carrying the evidence it actually has. Arena is built with bun, React 18 and Angular 22, and none of that is a requirement on you | | How do I make Arena look like my own product? | [`references/style-kernel.md`](./references/style-kernel.md), once per project and before the first screen | | Which of those answers actually change how it looks? | the same document's table, measured over eight products built on this kernel | | Does what I am building have to be found from outside it? | [`references/seo.md`](./references/seo.md), once per project: what Arena writes into the ``, and which layer writes it | | What questions does the kernel ask? | [`contracts/design/roles.json`](../../contracts/design/roles.json), one entry per role, with a type and a description and no value | | Does a component like this exist at all, and what if it does not? | [`frameworks/INDEX.md`](../../frameworks/INDEX.md), which also says which layers ship it. A no is one of three: your product is a different register and the markup is yours, [`references/media-register.md`](./references/media-register.md); Arena ships none on purpose, which [`references/surface.md`](./references/surface.md)'s last section states with the reason; or nobody has added one, which is the other branch, named at the top of this page | | Which category holds the component I am reaching for? | `frameworks//INDEX.md`, which names every one of them and describes none | | What is it called in my framework, what does it take, and where is its prompt? | `frameworks//components//INDEX.md` | | How do I use this component? | its `.prompt.md`, linked from that index | | What exactly does this member take? | the members table in that same prompt | | Why does this member exist at all? | `contracts/api/components/.json` | | What else does the package export, besides components? | the layer's `PACKAGE.md`: the theme surface, the two measurements, the chart ramp helpers, and Angular's projection markers | | How do I size a page layout, or fit a panel to its own box? | the same section: `useArenaViewportBelow` / `arenaViewportBelow` for a page, `useArenaContainerWidth` / `arenaContainerWidth` for a box | | What do I paint my page and my own markup with? | [`references/page.md`](./references/page.md): the floor, the nineteen colour roles, and why a role follows the skin where an alias follows the palette. **Arena paints no page of yours** | | How much air goes between two components, and what column does the page sit in? | the same document: the three named steps as classes and as tokens, the four classes the column is made of, and the cut between air you place and air the kernel answers | | What does Arena ship at all, and how much of it do I have to take? | [`references/surface.md`](./references/surface.md): every part in one table with the document that owns it, and three steps a project can stop at | | Where does Arena stop? | the same document's last section, which is a list of decisions rather than gaps waiting to close | | What is the value of a token? | the DTCG JSON for its group in `contracts/design/` (`ls contracts/design/*.json`), which is the machine-readable form and is cheaper than the specification below. Two files hold what DTCG cannot: `contracts/design/colors.css` and `contracts/design/environment.css`, both of which [`references/page.md`](./references/page.md) reads for you | | What does a value mean, and why is it that? | [`contracts/design/AGENTS.md`](../../contracts/design/AGENTS.md), the normative design specification | | What must this kind of component do to be accessible, and what does an Arena one promise? | `contracts/behaviour/.json`, one file per pattern and shipped inside your package. The page handing you the markup names the file it binds: [`references/media-register.md`](./references/media-register.md) for a feed, a lightbox or a viewer, [`references/seo.md`](./references/seo.md) for structured data. What one of Arena's own components does is its `.prompt.md`; the file recording which pattern it binds is what Arena is held to, not a document you read | | How do I install Arena in my app? | [`frameworks/react/PACKAGE.md`](../../frameworks/react/PACKAGE.md) or [`frameworks/angular/PACKAGE.md`](../../frameworks/angular/PACKAGE.md) | | What does every component look like at once? | `frameworks/react/kitchen-sink/`, and the same page in `frameworks/angular/` | | What does a token look like on screen? | `intro/guidelines/*.html`, the specimen cards | | Arena itself is wrong, or one of these pages sent me somewhere empty. Where does that go? | [`CONTRIBUTING.md`](https://github.com/dravensoft-dev/arena/blob/main/CONTRIBUTING.md), which takes an issue and a pull request alike, and says what makes either usable. A question this documentation did not answer is one of them | **Do not read these to build something.** `contracts/api/AGENTS.md`, `contracts/behaviour/AGENTS.md`, `frameworks/PACKAGING.md`, and each layer's own `AGENTS.md` are about *changing* Arena, not about using it. They are large, and none of them answers a question in the table above. ## Two ways to deliver **A visual artifact** (a slide, a mock, a throwaway prototype): copy the assets you need out of `assets/`, and write static HTML that links `intro/styles.css`. That one stylesheet pulls in every token, so the page is on-brand with no build step. It must be served over HTTP rather than opened from `file://`. **Production code**: use the component library for the consumer's framework, import from `@dravensoft/arena-react` or `@dravensoft/arena-angular`, and follow the prompts. **Put no class of your own on an Arena component**, and write no rule targeting one. A component renders `arena-__` class names, so a rule of yours can reach one by specificity; nothing stops you and nothing supports you either. The name reads like a BEM surface somebody meant you to target and it is not one: it is compiler output, no contract names it, and a slot may be renamed in any release. Content you draw yourself is yours, styled through the same tokens. **How a screen is composed is yours, and the silence is deliberate, and it is about the shape rather than about the inventory.** Arena ships the pieces and no shape they must make: a console and a reading view are different pages before they are different components. Start from what the application is, and take the pieces from [`references/page.md`](./references/page.md), which is the inventory half and holds every one of them. Arena keeps the questions and every answer is a style plugin. The reasoning under the appearance decision above is [`contracts/design/StylePlugins.md`](../../contracts/design/StylePlugins.md). ## Ask before you build **A brief names a product and never a palette, so the questions are always the same ones**, and a one-line brief is the normal case rather than the empty one. Which ones to ask, in what order, and what each answer unlocks is the tree named above. **Invoked with no guidance at all**, ask what the user wants to build and then walk that tree from its first node. Then act as an expert in the Arena language and produce either an HTML artifact or production code, whichever the answer calls for. # When the markup is yours Arena's component list is the furniture of an application somebody works in. A photo wall, a story ring, a feed of posts, a slideshow, a document editor and a game map are outside it, and the router says so before it says anything else. This page is what that sentence owes you. **The answer is not that Arena is the wrong choice.** The skin travels either way: a style plugin answers every role whatever the product is, and one of the products measured on this kernel is a photo feed. What changes is that you write the markup and Arena hands you the parts, so the question stops being "which component" and becomes "which of Arena's pieces does this element need". Read this once, when you have found the first screen with no component under it. ## Read the pattern before you write the markup An element Arena does not ship still owes a user everything Arena's own components owe them. `contracts/behaviour/` is where each pattern is stated once, normatively, in a form your own markup can be held to: - **`contracts/behaviour/feed.json`** for a scrolling list of posts: the `feed` role, an `article` per entry, `aria-posinset` and `aria-setsize` on each, `aria-busy` while a page loads, and the keys that move between articles. A feed of your own that binds none of this is a feed only a mouse can read. - **`contracts/behaviour/dialog-modal.json`** for a lightbox or a viewer: `aria-modal`, focus taken on open, focus restored to the invoker on close, Tab trapped inside, Escape out. Read the one your element is, and bind what it requires. These files are the same source Arena's own components are held to, so what you write is held to the standard the components are. ## One of those two is handed over, and the other is not **You do not write a focus trap.** The package exports the one Arena's own dialogs run on, and your layer's `PACKAGE.md` names each: the modal contract, the tone-to-colour map for a status shape you draw, and the visually-hidden style object for a label the design does not show. Reach for those rather than writing a second copy, and read that page's export table before you reach for anything else: what it names is what carries a promise. **The feed pattern has no such export, and it is not an oversight you can wait out.** Arena binds it once, inside `ArenaActivityFeed`, and that component is not your feed: it is an event log with a fixed row and no slot for your markup, so reaching for it because the name matches is the mistake this section exists to stop. What you can do is read how it binds the pattern, in that component's source, and write the same thing around your own article: the paging keys move focus between articles rather than scrolling, which is the part a reimplementation from memory gets wrong. Until an export exists, that reading is the handover. **`isArenaOwnActivation(target, container)`** is the one to know about by name. A post that opens on tap and carries a like button inside it is two activations on one surface, and that predicate is how the outer one keeps its hands off the inner. It is the same rule Arena's own clickable rows run on. ## The stylesheets are for content you draw The package ships more than the components' own CSS, and these four are the ones written for markup that is not a component: `css/rhythm.css` for the vertical stack between your elements, `css/page.css` for the column the page sits in, `css/numerals.css` for a figure that must not jitter as it counts, and `css/sr-only.css` for the label a screen reader needs and the design does not show. Your layer's `PACKAGE.md` has the whole tree and says which depth to pick. **The frame around a picture is a component, and the grid around the frames usually is not.** `ArenaFigure` is the cell: its `ratio` defaults to the `aspect-media` role and its overlay slot paints `overlay-media`, so the two roles below are answered for every picture at once and a wall reads as a wall rather than as whatever sizes the images happened to be. Use it for a post's image and for a wall's cell alike. The grid holding them is the split. `ArenaGrid` auto-fits off the `grid-min` role, which is what you want when the count is the data's and the plugin decides how dense it looks. A wall that is **three across because the design says three** is `grid-template-columns: repeat(3, 1fr)` on an element of your own, one line, and `ArenaGrid`'s own Don't says so: two products measured wanting a fixed count wrote exactly that line rather than bending a component into it. ## Four roles decide how your media looks The kernel's role list is where a project answers what Arena looks like, and four of its roles are about exactly this register rather than about the furniture: `aspect-media`, `fit-media`, `r-media` and `overlay-media`. Their descriptions in `contracts/design/roles.json` are written about galleries and shops. If your product is media, those four are not a detail of your style plugin, they are most of what a viewer will notice, so answer them first and look at the result before you answer anything else. `r-media` is also where the boundary is stated in the kernel's own words: a card is a container the reader looks past, and a figure is the thing being looked at. If your post is the thing being looked at, it is not a card. **Two component names will look like your answer and are not.** `ArenaActivityFeed` is an event log with a fixed row, and `ArenaCard` is the container above. Both are the first thing a search for your screen returns, and reaching for either is how a product in this register ends up fighting a component instead of writing six lines. ## The viewer that opens a picture at full size Arena ships no lightbox, and this is the element of your own that comes up most often. Every value it needs is already a token, so the only thing you are inventing is the box: - **The box needs no length of its own, and the one people write is the tell.** As tall as the viewport allows is not a token and is not a percentage of one: it is `position: fixed` with `inset: 0` on the layer and `max-width: 100%` with `max-height: 100%` on the picture, and the layer's own padding is where `--pad-safe-*` and a step of the spacing scale go. A chosen `90vh` is the shape this goes wrong in, and it is usually compensating for the second mistake rather than for a design: `vh` is the viewport a phone has with its browser chrome retracted, so a picture sized in it is taller than the room it has while the chrome is showing. `dvh` is the unit that follows the chrome, which is why `.arena-shell` fills the window with `100dvh`, and a fixed layer at `inset: 0` needs neither. There is no role here because there is no decision here: how tall a full-size picture may get is the viewport's answer and not a style plugin's. - **The dim is `--scrim` with `blur(var(--scrim-blur))`**, which is the pair Arena's own dialogs dim the page with, and the layer is `--z-modal`. A scrim of your own is the one value that makes a viewer look like a different product from the dialogs beside it. - **`object-fit: contain`, not `cover`.** Edge to edge and never cropped pull against each other on any viewport whose shape differs from the picture's, and a capture that lost a corner to its frame is one somebody has to go and find the original of. What is left over on the short axis is the dimmed page rather than a mat you designed. - **The way out is a control you draw, and `--overlay-media` is what it stands on.** That role is the wash a frame draws under anything laid over a picture, and a close control laid over one is that case even with no `ArenaFigure` in sight: the picture underneath is one nobody chose, so a control tinted by the palette alone is legible over some of them and invisible over the rest. - **`--pad-safe-top` and `--pad-safe-right`** hold that control off a phone's cutout, composed with a step of the spacing scale. - **Bind `contracts/behaviour/dialog-modal.json`** and take the focus trap your layer exports rather than writing one. A viewer with no name, no Tab trap or no focus restored to the control that opened it is the same defect in your markup that the contract exists to stop in Arena's. ## One rule bends here, and only one **One primary accent per view** is written for a screen. A feed is not a screen in that sense: a follow action on each of twenty posts is not twenty primary accents, it is one repeated element, and the rule is about the eye having one place to land rather than about a count of buttons. Give the repeated action a secondary or a ghost weight and keep the single primary for what the screen as a whole is for. Nothing checks this either way, so it is yours to hold. Every other rule the router states holds unchanged, and two of them get harder rather than easier in this register: no gradients on a surface, and a chart carries identity or meaning and never both. A media product is where both are most tempting. ## What Arena still gives you **The screen you are writing is mostly components with one element of your own in the middle.** The banner, the navigation, the states around empty and failed, every field and every button are furniture whatever the product is, and reaching for them is what keeps a product built this way from reading as two. Your layer's `INDEX.md` is the directory; work out from your own element rather than in from a page layout. # Before the first screen This page is where a project settles what it looks like, and its first node splits the two ways of arriving: a project with no `arena.config.json`, no style plugin and no screens, where the appearance is invented by whoever writes the first file, and one already carrying all three, where it is context to acquire rather than to invent. **Read this before any other page this branch routes you to**, because every one of them assumes the decisions below have been made. **It is a tree rather than a list.** Each node asks one question, and the table under it says where each answer goes. Take the row your answer matches, go to the node it names, and read nothing else: a node you did not reach holds a decision that is not yours to make yet, and reading it early is how an agent answers a question nobody asked. **Two rules hold at every node.** Write no file before the node that writes one. And where a row costs something, say that cost to the user in their own words and get an explicit yes before going on, because the cost is the concrete way that answer goes wrong and a warning nobody answered is a decision the agent made alone. ## 1. Does the project already carry Arena? | The answer | Go to | |---|---| | Yes, it has an `arena.config.json` and screens built on it | 7 | | No, or it has the package installed and nothing written against it | 2 | ## 2. Where does the appearance come from? | The answer | Go to | What it costs you | |---|---|---| | A document stating the palette, the type and the roles | 5 | nothing, and this is the case the rest of this page exists to reach | | A public URL to copy | 3, then 5 | every role that lives behind a state, and probably the register | | A screenshot | 3, then 5 | every role outside the one polarity, density and state it caught | | One of the registers Arena measured, picked rather than invented | 4 | the palette is still yours to state, and nothing else about the product is settled | | Nothing yet | 3 | the whole interview, which is the honest price | **A public site hides its own states.** Hover, focus, disabled, error, empty and loading are behind interactions a reading never performs, so every role about a state is guessed rather than read. A marketing page is also a different register from the product behind the login, and copying the first gives the second a skin it was never designed to wear. **A screenshot is one polarity, one density, one breakpoint and one state.** Worse, a colour read off a pixel is the composite of a surface, an opacity level and whatever was laid over it, so it enters the palette as a literal that no theme can move, and the second polarity inherits it and is wrong. Neither is a reason to stop. They are reasons to run node 3 first and to show the user what you concluded before you build on it. ## 3. The minimum interview Five questions, and they cover the decisions [`style-kernel.md`](./style-kernel.md) measured as the ones that separate one product from another. Ask them one at a time. Which colour the TEXT takes is not among them, because eight unrelated products answered that identically; which surface the page takes is settled by the first question rather than by a sixth of its own, because it follows from what the screen is. That page's own table is where both are. | Ask | What the answer fixes | |---|---| | What is being built, and who works in it | the register, whether Arena's component list is the right one, and `aspect-media` and `fit-media` | | Whose brand is this, which polarity leads, and which colour is the voice | the palettes in `arena.config.json`, both of them | | Which three faces: display, body, mono | `ff-heading`, `ff-body`, `ff-mono` | | What character does it have to the eye: soft, drawn, or flat | `r-*`, `bw-*`, `fw-*`, `tt-*` and `track-*`, `shadow-*`, `press-scale` and `lift-control` | | How dense is it, and how wide does it breathe | `pad-control-*` and `gap-*`, `container-max`, `measure-prose`, `grid-min`, `gutter` | The fourth is one question rather than four because those roles move together. A product is soft, or drawn, or flat, and a plugin that rounds the corners while keeping the hard border and the deep shadow reads as two products sharing a screen. | The answer | Go to | |---|---| | The user answers them | 5 | | The user will not answer them | 4 | ## 4. Start from a register Arena measured **Two arrivals reach this node**, one that would rather pick than answer and one that will not answer at all, and it is a real answer to both. Ask for one thing only: a short description of the app. Then match that description against the catalogue in `plugin-style-store/catalogue/`, where each directory is a register measured on a real product and carrying its own `ENTRY.md`. **Match on one line rather than on the entries themselves.** Every `ENTRY.md` carries a line beginning `Take this entry when`, which says what that register is for in the terms a description arrives in. Read those lines, pick the one the description fits, and only then open that entry. Read it, do not remember it. **Name the entry you picked and what it decides, and wait.** Say which register it is, what it answered for shape, air and depth, and what it leaves open. An entry chosen silently is the same failure as a screenshot read silently, with a better result and the same missing consent. **An entry is copied rather than cited.** Its `plugin.tokens.json` and its `plugin.css` become your project's own under `design//`, and node 10 is where they land. **The catalogue is in the Arena repository rather than in the package.** From an installed project, read it there. | The answer | Go to | What it costs you | |---|---|---| | The user confirms an entry | 5 | the palette is still theirs to replace, and nothing else is settled | | Nothing in the catalogue is close | 5 | the appearance Arena installs with, which every project on that step shares | ## 5. Write the identity document, and stop Whatever route reached this node, the output is the same: a page stating the palette in both polarities, the three faces, the character, the air, the media shape and the page shape, with the reason beside each. `arena-from-scratch/identity.html` in the Arena repository is the template, and `arena-from-scratch/identity.example.html` beside it is one worked answer to read first. It is a page rather than a list because a palette described in prose is a palette nobody can approve. Serve it over HTTP, show it to the user, and change what they change. **Then stop.** The next node writes files, and files written against an unapproved appearance are the ones that get rewritten. | The answer | Go to | |---|---| | The user approves the document | 6 | | The user changes something | 5, again, until they stop | ## 6. The four decisions a project makes once Each is settled before the first screen and never per screen. They are in this order because each one asks about something the next one assumes. | Ask | Read | Skip it when | |---|---|---| | What does Arena ship, and how much of it am I taking | [`surface.md`](./surface.md) | never, because the three below ask how much of a thing you have not been shown | | Is Arena's component list right for this product | [`media-register.md`](./media-register.md) | the register is an application somebody works in and nothing on screen is a wall, a feed, a viewer or a document | | How does it answer the kernel's roles | [`style-kernel.md`](./style-kernel.md) | node 7 found a plugin that already answers every role | | Does anybody outside it have to find it | [`seo.md`](./seo.md) | never, because a no that was never chosen is announced by nothing and reaches the install rather than a screen | Then go to 8, because the last of those four is a yes or a no and the next node is how much. ## 7. Read what is already there An identity somebody already established is context to acquire, not a step to skip. Before adding anything: - Read `arena.config.json`: which palettes, which polarity leads, which fonts, and which plugin is named first, since that one is the root and answers every role. - Read the project's own `plugin.tokens.json`: which roles it answers and with what. A role it does not answer is a property that disappears rather than a plainer look. - Read its `plugin.css`, if it has one, to see which decisions the project paints by hand. Those are the ones no role reaches, and they are the ones your new screen has to keep. - Run `arena-to-prod --audit` and report what it names before you write anything. It reads the project's own sources and its declared plugin directories for the rules source text can show. Say what you found. A project whose plugin answers every role needs no style plugin work, and saying so is what stops an agent rewriting an appearance somebody already chose. Then go to 6, and skip the rows it says you may skip. **Stop there rather than going on to 8**, because the nodes past it decide an install a project carrying Arena has already made: the route from here is the per-screen one in [`../SKILL.md`](../SKILL.md). Reach 8 only when this screen is the first that has to be found from outside the product. ## 8. How much does it have to be found? Node 6 answered whether anybody outside the product has to arrive at it. This asks how much, and it is a separate question because the answer decides a layer and a peer dependency rather than a screen. **Both layers draw the same components; only one of them writes the document ``.** | The answer | Go to | What it takes | |---|---|---| | Nothing. It is behind a login, or it is a console | 9 | neither layer, and no metadata anything. This is the honest answer for most tools | | A title and a description per route | 9 | either layer. Angular has it in the package; React carries it through whatever its own framework offers | | Everything: canonical, `og:*`, a default that keeps a route out of an index until it says otherwise | 9 | on Angular, `@dravensoft/arena-angular/metadata`, where `@angular/router` stops being optional; on React, your own framework's head, since Arena writes none there | **The third row is a decision about the framework, and it is worth making while it is still cheap.** `@dravensoft/arena-react` writes no `` at all, by design, so a React project answering that way carries the whole of it itself. The Angular entry point is a second one, apart from the one components come from, and reaching it is what pulls the router in: it is declared an optional peer for exactly this reason, so a project that never reaches for metadata never installs a router it does not use. [`seo.md`](./seo.md) is where the three properties behind that third row are written down, and node 6 sends every project there whatever it answered here. That is not a contradiction with this node: a no reached by choosing is a decision the project can defend later, and a no reached by never asking is announced by nothing. Read it on the third row to learn what you are taking on, and on the first to learn what you are declining. ## 9. What is it built on? **Arena takes no position on how an application is assembled**, which is a real property rather than a slogan: its React layer imports `react`, `react-dom` and its own two bundled styling utilities and nothing else, and neither layer ships a router, a store or an application shell. What follows is what that buys per architecture, and where it stops. | The answer | Go to | What is true, and what you carry | |---|---|---| | A single-page application | 10 | the ordinary case, and everything below is a variation on it | | Server rendering with hydration | 10 | supported. React's own suites server-render, and neither layer reads a browser global at module scope | | Static generation or prerender | 10 | the components take the same path they take under server rendering. Arena ships no prerender tooling, so producing the pages is your framework's job | | React Server Components | 10 | **Arena ships no `use client` directive**, so the boundary is yours to draw, and it is one line | | A microfrontend inside a host | 10 | the components are safe; the stylesheet is document-wide. Read the paragraph below before choosing this | **Server rendering is the claim with the most evidence behind it.** Much of the React suite renders through `react-dom/server` on every run, so the layer is exercised rather than merely believed: there is no `useLayoutEffect` anywhere in it, and the only module-scope touch of `document` is behind a `typeof document === 'undefined'` guard. Angular reaches the document through the injected token rather than the global, and its two measurements run after render, which is the same property arrived at by a different route. One honest asymmetry: React's server rendering is held by a suite and Angular's is held by its code, so the second is the one to smoke-test first in a project that depends on it. **One hydration behaviour to know**, since nothing reports it: the viewport measurement answers false on a server, so a component branching on it renders its wide branch into the HTML and corrects after hydration. Branch on a container rather than the viewport where the difference would be seen. **React Server Components need one line from you.** Arena's components use hooks, and no file in the package carries `'use client'`, so importing one directly into a server component fails. Put the directive at the top of your own module that imports Arena and the boundary is drawn where you meant it, which is better than the package drawing it for you at every leaf. **A microfrontend is the one where the answer is split.** The JavaScript is genuinely architecture-neutral. The stylesheet is not: the tokens are declared on `:root`, and the reset sets the box model on `*` and a line height on `html`, so loading Arena inside a host page changes that page and not only your subtree. Two different Arena versions on one document collide on `:root`, and the last stylesheet loaded wins. What does work in your favour is that a palette other than the default emits as a plain class, so your own subtree can carry its own theme without touching the host. Take this row knowing the host is being changed, or give the fragment a document of its own. ## 10. Now write it **What the answers made necessary.** Nothing here is a preference, and no further question about dependencies is the skill's to ask: past this table the stack is the project's own. | Always | Which layer | Why | |---|---|---| | `@dravensoft/arena-react` **or** `@dravensoft/arena-angular` | one of them, never both | the layer nodes 8 and 9 settled. This is Arena itself; everything under it is a peer, and a list of peers with the package missing installs nothing that draws | | `@phosphor-icons/web` | both | icons are class-name strings a component renders, and the font is never bundled | | `@angular/cdk` | Angular only | a peer of that layer, and **not** something a React project installs | | `@angular/core`, `@angular/common`, `@angular/platform-browser` | Angular only | the peers the layer is built on | | `react`, `react-dom` | React only | the peers that layer is built on | | Added by an answer | When | From which node | |---|---|---| | `@angular/router` | the third row of node 8, on Angular | it is an optional peer until the metadata entry point is reached, and reaching it makes it required | **Any of npm, bun or pnpm installs this.** The packages declare their peers rather than assuming a flat tree, and pnpm's strict layout is the one worth naming because it is the one that would break a package that assumed otherwise: installed under it, the command below resolves the icon font through the symlinked store and writes both stylesheets. Nothing here needs a hoisting flag. Then, in this order, because each file is read by the next. - `arena.config.json`, with both palettes, the three font slots, and `stylePlugins` naming the directory the next two bullets write, as `["./design/"]`. **A plugin the config never names is a plugin nothing loads**, and nothing reports it: the build succeeds and the product wears the answers Arena installs with, which are Dravensoft's. `["default"]`, or leaving the key out, is that same appearance chosen on purpose rather than by omission, and it is a finished answer for a first screen or a tool nobody outside the team looks at. - `design//plugin.tokens.json`, answering every role. Shapes first, then space, then weight, then depth, and leave the colour roles at the answers [`style-kernel.md`](./style-kernel.md) reports eight products converged on. - `design//plugin.css`, only for a decision no role reaches. - Run `arena-to-prod`, which writes the stylesheet no package can carry and subsets the icon font to the glyphs your screens draw. Import what it wrote. - Then the first screen, through the per-screen route in [`../SKILL.md`](../SKILL.md). The identity document from node 5 is what each of these is checked against, and it stays in the project: the next agent to open it starts at node 2 with the good answer instead of at node 3. # What you may choose **Arena is built with bun, React 18, Angular 22 and TypeScript. None of that is a requirement on you**, and this page exists because the two are easy to read as one thing. What a design system is built with leaks into its documentation as a default nobody chose, and an agent that inherits it picks a toolchain for the wrong reason: not because the product needs it, but because the sample command happened to be written that way. So this page is the repertoire. Every axis a project settles before its first screen, every answer Arena supports on that axis, and **how much evidence each answer actually has**. Read this once per project, beside [`cold-start.md`](./cold-start.md) and before the first screen, and choose each axis on what the product needs rather than on what Arena happens to be made of. ## The evidence column, and why it is there A yes with nothing behind it is worth less than an honest maybe, so no row here says yes. Each one says how it is known. | Evidence | What it means | |---|---| | held by a gate | proved on every run of Arena's own board. If it broke, a build here would fail before a release went out | | exercised by Arena's own suites | the code path runs here, repeatedly, against this version | | verified by hand once | somebody installed it that way and it worked. Nothing revalidates it, so it is a record rather than a guarantee | | allowed by the manifest, not exercised | the declared range resolves it and nothing is known against it. Nobody has run it | | does not work | with the reason, so you can plan around it instead of discovering it | **A row is never promoted without being exercised.** "Allowed by the manifest, not exercised" is a finished answer, and reading it as a soft yes is the mistake this column exists to prevent. If you need one of those rows to be certain, run it early and small rather than late and whole. ## The repertoire **Which package manager installs this?** | You may choose | Evidence | What is true | |---|---|---| | npm | verified by hand once | the packages declare their peers rather than assuming a flat tree, so no hoisting flag is needed | | pnpm | verified by hand once | the strict layout is the one worth naming, because it is the one that would break a package assuming otherwise: installed under it, the command still resolves the icon font through the symlinked store | | bun | verified by hand once | the same install. Bun is also what Arena is built with, which is a fact about Arena rather than a requirement on you | | yarn | allowed by the manifest, not exercised | nothing here works against it: the peers are declared, there is no install script to trust, and no flat tree is assumed. Nobody has run it, which is why it says allowed rather than yes | **What runs the command?** | You may choose | Evidence | What is true | |---|---|---| | Node >=22 | held by a gate | the command is a Node program, and that floor is the oldest line Node still supports rather than a capability the command needs | | any runner that reaches a Node program | allowed by the manifest, not exercised | npx and bunx both reach the same file. No runner is required: the command imports three node modules and no runtime API of its own | **How is the package loaded?** | You may choose | Evidence | What is true | |---|---|---| | an ES module import | held by a gate | the package is a module, and every target its exports name resolves to a file that is there | | a CommonJS require | does not work | there is no require condition and no second build. A project that cannot load a module cannot load this one, and it fails at the first import rather than somewhere later | **How does the stylesheet reach the page?** | You may choose | Evidence | What is true | |---|---|---| | an import from a module of yours | verified by hand once | the idiom this page shows. It is an instruction to a bundler rather than to TypeScript, which is why a project may need a declaration before it typechecks | | an @import from a stylesheet of yours | allowed by the manifest, not exercised | what the package hands you is a barrel of relative imports, so whatever resolves those resolves all of it | | the global styles list a framework CLI takes | allowed by the manifest, not exercised | the same file named as a global style rather than imported from a module, which is how a CLI-driven workspace usually takes one | | a link element | allowed by the manifest, not exercised | this one needs the package directory served as it stands, because the barrel reaches its parts by relative path | **What assembles it?** | You may choose | Evidence | What is true | |---|---|---| | any bundler that resolves exports and imports CSS | allowed by the manifest, not exercised | nothing in the package is written for one: no raw-text import, no build-time environment global, and nothing compiled at install time | | a framework CLI | allowed by the manifest, not exercised | where a layer ships in the package format its framework defines, partially compiled, the consumer build finishes it the way it finishes any library it depends on | | no bundler at all | allowed by the manifest, not exercised | the stylesheets are compiled already and the components are plain modules, so what a project needs is something that resolves a module specifier | **Which layer, and which version of it?** | You may choose | Evidence | What is true | |---|---|---| | React 18 | exercised by Arena's own suites | the version Arena builds and renders against on every run, server rendering included | | React 19 | allowed by the manifest, not exercised | inside the range the package declares, and not exercised here. Nothing is known against it | | Angular 22 | exercised by Arena's own suites | the version the layer is compiled and tested against, shipped partially compiled so your own build finishes it | | Angular 20 or 21 | allowed by the manifest, not exercised | at or above the floor the package declares, and not exercised here | | neither layer | allowed by the manifest, not exercised | the tokens, the stylesheets for markup of your own, the behaviour contracts and the command are framework-neutral. What you do not get is a component, so every element is yours to write and yours to hold to a pattern | ## What each layer requires Peer dependencies are declared rather than bundled, so your project installs them and your project decides their versions inside the ranges below. **A list of peers with the Arena package missing installs nothing that draws**, and an Arena package with a peer missing fails at the first import. | Package | Peer | Range | Required | |---|---|---|---| | `@dravensoft/arena-react` | `react` | `^18 \|\| ^19` | always | | `@dravensoft/arena-react` | `react-dom` | `^18 \|\| ^19` | always | | `@dravensoft/arena-react` | `@phosphor-icons/web` | `^2.1.2` | always | | `@dravensoft/arena-angular` | `@angular/core` | `>=20` | always | | `@dravensoft/arena-angular` | `@angular/common` | `>=20` | always | | `@dravensoft/arena-angular` | `@angular/platform-browser` | `>=20` | always | | `@dravensoft/arena-angular` | `@angular/cdk` | `>=20` | always | | `@dravensoft/arena-angular` | `@angular/router` | `>=20` | until you reach what needs it | | `@dravensoft/arena-angular` | `@phosphor-icons/web` | `^2.1.2` | always | Two of those rows carry more than a number. **The icon font is a peer in both layers.** Icons are Phosphor class-name strings a component renders, and no package bundles the font, so a project that skips this peer renders components with empty squares where the icons go and nothing reports it. **One peer is conditional.** The router is required only once a project reaches the entry point that writes the document head, which is why it is declared optional: a project that never reaches for metadata never installs a router it does not use. [`seo.md`](./seo.md) is where that decision is made, and node 8 of [`cold-start.md`](./cold-start.md) is where it is reached. ## What this page does not decide **Which layer you take, and how the application is assembled.** Those are questions with their own nodes, and duplicating them here would give a project two places to answer one thing. Node 8 of [`cold-start.md`](./cold-start.md) decides the layer, because how much of the product has to be found from outside it is what settles both the layer and one peer. Node 9 of the same tree answers the render architecture, a single-page application, server rendering with hydration, static generation, server components or a microfrontend, with what is true of each and what you carry. **How much of Arena you take.** [`surface.md`](./surface.md) is the whole offer in one table, with three steps a project may stop at and the line where Arena stops on purpose. **What your product looks like.** Nothing on this page touches appearance. [`style-kernel.md`](./style-kernel.md) is that decision, and it is independent of every axis here: the same style plugin travels whatever installs it and whatever assembles it. ## Where the repertoire stops - **No matrix is installed on every run.** The package-manager and bundler rows are claims about somebody else's tool, and nothing here revalidates them per release. That is why they carry the evidence they carry rather than a stronger word. - **No version is pinned upward for you.** The React range is closed at the majors that have been rendered, because an open range would promise a major nobody has seen. The Angular floor is open above it, so a version past the exercised one resolves and is untested: that is a permission, not a promise. - **No project of yours is read.** No gate here opens your application, so every rule of the language holds because you hold it. The closest thing is the command's own audit, which reads your source text for the five rules source text can show and reports rather than fails unless you ask it to. # Making Arena look like your product Arena keeps the questions and a repertoire of values. **Every answer is a style plugin**, and the appearance Arena installs with is one of them, so replacing it is the ordinary case rather than an escape hatch. **Eight registers are already measured, and picking one is a way to answer this page rather than a way around it.** `plugin-style-store/catalogue/` holds a directory each, and the fourth node of [`cold-start.md`](./cold-start.md) is where a product is matched against them. Read this once per project, before the first screen. The rules of the language are in [`../SKILL.md`](../SKILL.md) and hold whatever your appearance is. ## The three files you write **`arena.config.json`, in your project root.** The palettes, the fonts and the plugins, and nothing about components. **The shape, abridged, and not a file to copy**: every palette answers the whole colour set rather than the three shown, and a config carrying three keys is refused rather than defaulted. ```json { "stylePlugins": ["./design/notion"], "palettes": [ { "name": "light", "default": true, "polarity": "light", "colors": { "base-100": "#ffffff", "base-content": "#37352f", "primary": "#0b6bcb" } } ], "fonts": { "display": { "family": "Inter", "src": "..." } } } ``` **`"stylePlugins": ["default"]` is a legitimate answer**, and it is the right one for a first screen or a tool nobody outside the team will look at: it keeps the appearance Arena installs with, and a project moves off it when the appearance starts mattering rather than before. What this page costs you is paid when you decide to look like yourself, not to start. Arena ships two themes and is dark first, so a real config carries a dark palette as well as the light one above. The full colour list, the font slots and the rest of the config are on your package's own page, and the file to copy is there rather than here: [`../../../frameworks/react/PACKAGE.md`](../../../frameworks/react/PACKAGE.md) or [`../../../frameworks/angular/PACKAGE.md`](../../../frameworks/angular/PACKAGE.md). **`design//plugin.tokens.json`.** One entry per role, and the directory name becomes the plugin's name: ```json { "r-surface": { "$type": "dimension", "$value": "{r.xs}" }, "fw-heading": { "$type": "fontWeight", "$value": "{fw.bold}" }, "fill-field": { "$type": "color", "$value": "{color.base-200}" }, "press-scale": { "$type": "number", "$value": 0.98 }, "container-max": { "$type": "dimension", "$value": { "value": 920, "unit": "px" } } } ``` **`design//plugin.css`, optional.** CSS of your own, selecting the part hooks: ```css [data-arena-part="side-nav.item"] { color: color-mix(in oklab, var(--color-base-content) 82%, transparent); font-weight: var(--fw-control); } ``` The eight products measured for this document write between two and sixteen such rules each. If yours is running to hundreds, the answer is almost always a role you have not moved. ## The two rules that decide everything else **The first entry in `stylePlugins` is the root plugin, and it answers every role.** It emits on `:root`, and a custom property with no value is invalid at computed-value time: the declaration reading it is dropped and the property disappears. A partial root plugin is not a plainer appearance, it is a page with no borders. Every later entry emits under `.arena-`, taken from its directory, and is a difference sitting over the root plugin, so it answers only what it changes. **A colour role takes one of your palette colours as a `{color.*}` alias, and never a literal.** The palette is yours and what a role says is WHICH of your colours a surface takes. It is mechanical as well: a bare colour alias is restated under every palette, and anything else resolves to one theme's value and inherits it into the other, so a hex here is a light-mode colour showing through your dark theme. ## What an answer may be - **A scale alias**, `{r.xs}`, `{sp.4}`, `{fw.bold}`, `{dur.fast}`, `{ease.out}`. This is the ordinary answer. - **A `{color.*}` alias**, for every colour role. - **A literal**, where the scale has nothing you want: `{ "value": 920, "unit": "px" }`, or a bare number for a ratio such as `press-scale`. **Two of the loudest decisions need one**, because the scale has no step for them: a square corner is `{ "value": 0, "unit": "px" }`, and no shadow at all is a shadow object whose every length is zero and whose colour is fully transparent. - **A unit the type does not carry**, through `$extensions`. Four roles need it and the rest do not: `track-heading`, `track-eyebrow` and `track-label` take `em`, and `measure-prose` takes `ch`. A tracking role that forgets it emits a bare number, which is not a valid letter spacing, and the declaration silently resolves to `normal`. ```json "measure-prose": { "$type": "number", "$value": 86, "$extensions": { "com.dravensoft.arena": { "cssUnit": "ch" } } } ``` **The type of a role does not tell you which scale its answer comes from**, so here is the map. A `dimension` role draws from four different places depending on what it is for: | Roles | Scale | Where the steps are | |---|---|---| | `r-*` | `r` | [`effects.json`](../../../contracts/design/effects.json) | | `bw-*` | `bw` and `bw-strong`, a pair rather than a ladder, so the alias carries no step | the same | | `shadow-*` | `shadow` | the same | | `dur-*`, `ease-*` | `dur`, `ease` | the same | | `pad-*`, `gap-*`, `gutter` | `sp` | [`spacing.json`](../../../contracts/design/spacing.json) | | `step-eyebrow`, `step-label` | `dz` | the same | | `fw-*` | `fw` | [`typography.json`](../../../contracts/design/typography.json) | | `ff-*` | `font`, which resolves to the families your config declares | the same | | `track-*` | `ls` | the same | | `lh-*` | `lh` | the same | | `step-title-*` | `fs` | the same | | `container-max`, `grid-min`, `measure-prose`, `lift-control`, `press-scale`, `aspect-media` | none | a literal is the only answer | [`Scales.md`](../../../contracts/design/Scales.md) beside them is the reasoning for the ladders that have one, which is not all of them. **What an answer may never be is a scale itself.** A scale step is shared by every use that wants that value, so moving one is not a style plugin but a different Arena. **The type and page-rhythm ladders are the one exception**, because they reach a page through classes you apply rather than through any role, and you answer them in the plugin beside the roles: a key named `fs-` or `rhythm-` is read as that step rather than as a role nothing ships. Both are dimensions and both carry a `$description`, because a step a plugin moves is a decision rather than a value. Move one step or every one of them, as the product needs: completeness binds a role, which disappears unanswered, and never a step, which keeps the value Arena ships. Every other scale key is refused by name, so a different type scale is answered here or it is not answered. The questions themselves are in [`../../../contracts/design/roles.json`](../../../contracts/design/roles.json): a name, a type, a description and, for a keyword role, its closed set. No values, because the values are yours. ## Where the appearance actually comes from Eight unrelated products were built on this kernel, one each in the register of a project tracker, a language course, a photo feed, a document workspace, a storefront, an inbox, an observability console and a booking flow. **Every one of them answered these identically**, and a plugin that spends its first day on them spends it on nothing: | Answered the same way by all eight | What they said | |---|---| | `ink-heading`, `ink-body`, `ink-muted` | `{color.base-content}` | | `ink-eyebrow` | `{color.neutral-content}` | | `edge-surface`, `edge-surface-floating`, `edge-control-quiet`, `edge-separator` | `{color.base-300}` | | `ff-heading` | the display face the config declares | | `lh-heading`, `dur-hover`, `ease-hover` | one step each, the same one every time | **Which colour the text takes is a constant, and which colour a surface takes is not.** The ink roles and the four quiet edges are what eight brands converged on, and starting from them costs nothing. The fills are the opposite, and it took the wider catalogue to show it: a console puts its panels on the second surface step and its page on the first, a booking page does the reverse, and the other six leave both at the page colour. **That answer follows from the page shape rather than from the palette**, so settle what the screen is before assigning any of it. Those are not the answers the appearance Arena installs with gives. That one is a design of its own and assigns several of these differently, so it is a plugin to read rather than a baseline to inherit: a plugin you write answers every role itself, and nothing is inherited from it. It is [`plugin-style-store/default/plugin.tokens.json`](../../../plugin-style-store/default/plugin.tokens.json) in the Arena repository, and [`plugin-style-store/complete/`](../../../plugin-style-store/complete/plugin.tokens.json) beside it is a second one answering the same roles differently. Read one of them before writing yours: answering every role from a list of role names is the hardest instruction on this page, and a worked answer is worth more than the list. **Both are in the repository rather than in the package**, so from an installed project read them on the Arena repository, or start from `"stylePlugins": ["default"]` and replace one role at a time against something that already runs. **These are where a product lives**, and the eight measured products disagree on every one of them: | Role | What moving it decides | |---|---| | `r-control`, `r-field`, `r-media` | how round the product is, which is the loudest single decision | | `bw-surface`, `bw-control`, `bw-field`, `bw-separator`, `bw-marker` | whether an edge is a hairline, a drawn line, or absent | | `fw-heading`, `fw-eyebrow`, `fw-control` | how loud type is before any size changes | | `tt-label`, `tt-eyebrow`, `track-label`, `track-eyebrow` | whether small text shouts in capitals or reads as a word | | `pad-control-x`, `pad-control-y`, `gap-inline`, `gap-items`, `gap-control` | how tight the product is to the hand | | `container-max`, `measure-prose`, `grid-min`, `gutter` | how wide it breathes, and it ranges from a dense console to a reading column | | `shadow-surface-rest`, `shadow-surface-floating`, `shadow-surface-deep`, `shadow-control-raised` | whether depth is a soft blur, a hard offset or nothing at all, and whether a surface has any at rest | | `press-scale`, `lift-control`, `dur-state`, `ease-state` | how the product answers a hand | | `aspect-media`, `fit-media` | what shape an image is, before any of it is cropped | | `fill-page`, `fill-surface`, `fill-surface-sunken`, `fill-field` | which surface is the page and which is the thing standing on it | | `edge-control`, `edge-field`, `edge-marker` | whether a control, a field and a marker are outlined at all, and in what | | `ff-eyebrow`, `ff-label` | whether small text is the body face or the mono one, which is the difference between a shopfront and a console | **So the order of work is: shapes, then space, then weight, then depth.** Leave the colour roles where they are until those read as your product, because a plugin that starts by reassigning colours changes the least and costs the most to undo. ## The part hooks Every element drawing a slot carries `data-arena-part="."`, and that is what your `plugin.css` selects. **The hook is an attribute on the element, so the page you are looking at is the list**: serve the application, inspect the element you want to paint, and take the value. Nothing enumerates the hooks in prose, and a name guessed from a component's member list is a selector that matches nothing and reports nothing. **You never write `@layer`**: the build wraps your sheet in a reserved layer declared after the compiled component rules, so an ordinary selector wins with no `!important` anywhere, and the sheet restates the layer order at its own head so a bundler cannot reorder it into losing every contest silently. Your application's own unlayered CSS still beats the plugin layer, and that order is right: your application is the last word. It is also why reaching into an Arena slot from application CSS is reported. It works, and working is what makes it debt rather than an error. ## Gotchas Each of these was measured on a product, not reasoned about. - **A rule that restates what the slot already paints changes no pixel.** It also inflates the count of parts your plugins paint, which is the number a request for a new role is argued from. The report names a restatement. - **Arena's own colour aliases are a step of Arena's ramp under another name.** A rule reading `var(--mute)` assigns a colour without answering any role, and the raw-colour rule stays quiet because an alias is a token. Answer the role instead, or compose the shade out of your own palette: `color-mix(in oklab, var(--color-base-content) 62%, transparent)`. - **A level is not a colour.** A colour role names which of your colours, never how far back it is held, and a slot that carries no opacity modifier cannot be held back by any answer to any role. Composing it in your own CSS is the route. - **The charts that draw geometry from the data carry no part hook at all**, because their coordinates are the data and they have no slots to hook. A plugin reaches the frame around a chart and nothing inside the plot; the palette is what skins a plot. - **A brand whose mark IS a gradient declares it once**, with `"gradientMark": true` in `arena.config.json`. The alternative is a marker on every line that draws the mark, and a marker suppresses every rule on its line rather than the one it is about. - **Holding the report in CI takes kinds, not a switch.** A brand under 4.5:1 is a decision you already made and measured, and one switch over every kind would make that the price of holding the rest. - **A part that exists in a slot and never in the rendered page passes every check there is**, because every check over a part reads text. Open the page and look at it. - **The screen-reader-only idiom ships as a class**, so a rule selecting a part cannot compose it. A part you want hidden visually and kept for a reader is hidden by hand. ## What the kernel does not reach Stated so you can plan around it rather than discover it. - **Press travel and a resting edge have no role.** A control that stands on a hard bottom edge and drops into it when pressed is one product's whole identity, and it paints that by hand. `lift-control` and `press-scale` are the near miss and do not cover it. - **A size ramp is not yours to re-answer**, the type and page rhythm ladders aside. Where a component's own ramp runs out, the exit is your own CSS on your own element, not a step moved under the kernel. - **A document block family is markup you write.** Paragraph, heading, quote, callout, toggle, code and figure are the register of a document editor, and Arena is a product-application library. ## The loop **Some of this is refused rather than reported, and knowing which is which saves you an afternoon.** A role left unanswered by the root plugin, a value of the wrong type, a colour role answered with anything but a `{color.*}` alias, an alias that resolves to nothing, and a broken reading floor are all **hard failures of the plain command**: it writes nothing and exits non-zero. Everything the audit says is a **report**, and stays one until you pass `--strict`. The reading floors are the trap in that list, because an ordinary-looking scale step can break one. Prose leading holds at 1.5 or more, heading leading at 1 or more, and the prose measure between 45 and 90. `{lh.tight}` is a perfectly normal step of the leading scale and it is under the heading floor, so answering `lh-heading` with it refuses the build. Beyond those, no gate reads your application, so the last step is the only one that checks the thing you care about. - [ ] Answer every role in `plugin.tokens.json`, starting with the shapes. - [ ] Run `arena-to-prod --audit`. Your plugin directory is in scope because the config declares it, so the audit measures your application and your plugin together. - [ ] Read the report. Fix what it names, and re-run it until it names nothing. - [ ] Import the three files it writes, `arena.generated.css`, `icons.generated.css` and, when your plugin carries CSS, `plugin.generated.css`. - [ ] Serve the application and look at it, in both polarities. ## How a role gets added Paint the decision by hand through the part. The report names every part your plugins paint, and that note is the evidence: **a role is added when several style plugins are measured painting the same decision through the same part.** What one product paints is its own. Two things that look like evidence and are not. A rule that restates the slot's own answer raises the count without changing a pixel. And an ask whose whole content is a difference between two states of one part cannot become a role at all, because a plugin selects by part and never by the value of a variant: the component renders one part in both states, and a plugin paints it once. # When it has to be found Arena writes the document `` in Angular, and whether your product needs that is a decision about the product rather than about a screen. Read this once per project, before the first screen. The rules of the language are in [`../SKILL.md`](../SKILL.md) and hold whichever way this one is answered. ## The question, and no is a real answer Ask whether somebody who is not already using this product has to arrive at it from outside. A console, an admin tool and anything behind a login are a legitimate no, and it is because no is so often the true answer that Arena publishes nothing until it is told to. A catalogue, a landing page, a public listing and a documentation site are a yes, and a yes has consequences before the first component. ## Arena writes the head in one layer, and the router is why `@dravensoft/arena-angular/metadata` is a second entry point of the Angular package, apart from the one every component comes from. It is apart because reaching it means reaching `@angular/router`, declared as an optional peer for exactly this reason: a project that reaches only for components never installs a router it does not use. **`@dravensoft/arena-react` has no counterpart.** It writes no `` at all, so a React project carries that itself, through whatever its own framework offers, and a React framework picked to be found generally offers one already. **What this decides is one piece rather than the stack**: on Angular Arena supplies it and on React your own framework does, so a yes here costs a dependency on one side and a wiring you were going to have anyway on the other. How the application is assembled stays the project's answer, in the ninth node of [`cold-start.md`](./cold-start.md), which is also where the asymmetry this page's reader is most exposed to is written down: a product that has to be found is a product that server-renders, and the two layers do not hold that claim with the same kind of evidence. Read it on the day the choice is still cheap. ## Three properties to know before you write a route **Every route is private until one of them says otherwise.** A screen that should be found says so itself, and the rest stay out of an index without anybody remembering to keep them out. Nothing announces the default, so the first evidence of forgetting it is a page missing from a result nobody thought to check. **A canonical needs an address you supply.** Arena refuses to derive one from the document, because such a value disagrees between a server render and the client that hydrates it, and a disagreement there is invisible to whoever introduced it. Say where the application lives, once, and the canonical and the `og:url` beside it both appear. **A title composes rather than competes.** Angular's own `title` on a route keeps meaning what it means and gains the suffix and the description beside it, so nothing already written gets rewritten to suit Arena. The exports carrying all three, with what each one takes, are on your package's own page: [`../../../frameworks/angular/PACKAGE.md`](../../../frameworks/angular/PACKAGE.md), under the heading about the ``. Open it when the answer is yes. ## What both layers publish `ArenaBreadcrumbs` describes the trail it draws in `schema.org` terms beside the markup, under both frameworks, and it changes nothing a person sees. Pass it `origin` and every crumb is published at an absolute address; without one the addresses go out as written, which readers support less well. Its own prompt is where that member is documented. `contracts/behaviour/structured-data.json` is that pattern stated on its own, and it is what markup of yours binds when it describes a structure worth handing to a reader rather than only to a person: a script of type `application/ld+json` next to the markup, the same structure in `schema.org` terms, and `<` escaped in the serialisation so no value you supply can close the tag. It is the one pattern in `contracts/behaviour/` that is not an accessibility requirement. ## What Arena does not decide Arena renders no page and lists none. It writes the `` of whatever page your application renders and describes the structures it draws; how that page reaches a reader, and what a crawler is handed when it asks for one, stay yours to answer. Which architecture produces it, and the evidence behind each, is the ninth node of [`cold-start.md`](./cold-start.md). The three properties above are the part Arena holds, and they are not the whole of the job. # What Arena hands over Arena is larger than the component list it is known for, and the parts that are not components are the ones a project discovers late or never. This page is the whole offer in one place, the line where it stops, and the one document that owns each part. Read this first, before the three project questions the router asks next, because those three ask how much of a thing you have not been shown yet. Nothing here teaches a part: every row names where the part is written down, and that document is where you go once you know you want it. ## Everything Arena ships | what it is | what you get | where it is written down | |---|---|---| | **The palette and the polarity** | The colours a build carries, declared by you: the base surface scale, the two accents, the four status colours, each of those with the colour that goes legibly on top of it, and the eight ramp slots. One palette reaches `:root` and every other becomes a class you put on ``, so a build carries as many skins as it declares. Each one states whether it is dark or light, and that answer is what a first visit is matched against. | your layer's `PACKAGE.md`, the section about declaring a skin | | **The token layer** | Every value the components read, as custom properties you may read too: the spacing scale `--sp-*` in thirteen steps, the type ladder `--fs-*` with `--fw-*`, `--lh-*` and `--ls-*` beside it, the corners `--r-*`, the borders `--bw-*`, the shadows, the scrims, the durations `--dur-*` and curves `--ease-*`, the stacking order `--z-*`, the three breakpoints `--bp-*`, the control and row sizes `--dz-*` that the density classes re-answer, and the avatar and logo sizes. | the DTCG JSON per group in `contracts/design/`, with [`contracts/design/Scales.md`](../../../contracts/design/Scales.md) for what each step is for | | **The colour aliases and the ink levels** | Short names over the palette for markup of your own: `--crimson`, `--gold`, `--danger`, `--success`, `--warning`, `--info`, `--bone`, `--mute`, `--bg`, and the soft washes beside each accent. The percentages the held-back text registers stand at are tokens rather than literals, so a palette moves them. | [`contracts/design/colors.css`](../../../contracts/design/colors.css) | | **The style kernel** | Seventy-two questions about shape, space, weight, depth and which colour a surface takes, answered by a style plugin your project writes. This is the part that decides whether the product looks like yours, and it is not the palette. | [`style-kernel.md`](./style-kernel.md), over [`contracts/design/roles.json`](../../../contracts/design/roles.json) | | **Two themes and two densities** | Dark and light without a component being rewritten, because a component reads roles. `.arena-compact` re-densifies the controls and the rows for a screen that has to hold more; `.arena-comfortable` grows them to a 48px touch target for a screen a thumb drives. The two are exclusive, and either is a class on an ancestor rather than a prop on anything. Density is an axis of its own rather than part of the style plugin, because how large a control is answers who is pointing at it and not what register the product speaks in: a shop on a phone and a console on a desk can want the same style plugin and opposite densities. Each re-answers the control and row sizes and nothing else, so the rhythm between two components stays where you spent it, and a screen that wants one on the desk and the other in the hand swaps the class from the breakpoint helper its layer exports, since a media query can add no class. | the rules section of [`../SKILL.md`](../SKILL.md) | | **The components** | The furniture of an application somebody works in, filed under seven categories: `brand`, `charts`, `display`, `feedback`, `forms`, `layout` and `navigation`. Every one ships under both frameworks from one API declared once, and every one names the accessibility pattern it binds. | [`frameworks/INDEX.md`](../../../frameworks/INDEX.md) for whether one exists and which layers ship it, then your own layer's index | | **Stylesheets for markup that is not a component** | The air between two of your elements, the column a page sits in, the measure of a reading column, a figure that does not jitter as it counts, and a label only a screen reader gets. These are what you build a page out of, and none of them is a component. | [`page.md`](./page.md) | | **Exports that carry a promise** | The focus trap Arena's own dialogs run on, the two activation predicates, the notice queue and its dismissal rule, the chart ramp helpers, the tone-to-colour map, the two measurements, the visually-hidden style and the id the main landmark writes. Reach for these instead of writing a second copy: what your layer's page names is what carries a compatibility promise, and a symbol found by autocomplete is not. | the export table on your layer's `PACKAGE.md` | | **The head, in one layer only** | A title that composes with the router's own, a description, a canonical and the `og:*` pair beside it, and a default that keeps a route out of an index until it says otherwise. Angular alone, behind a second entry point. | [`seo.md`](./seo.md) | | **The command** | `arena-to-prod`, which turns your config into the stylesheet no package can carry, subsets the icon font to the glyphs your screens actually draw, reports where your own sources break a rule of the language, and names the components you have not used yet. | the build section of your layer's `PACKAGE.md` | | **The behaviour contracts** | Each accessibility pattern stated once, normatively, in a form markup of your own can be held to: the roles it carries, the keys it answers, where focus goes and what dismisses it. Plus one that is not an accessibility pattern, for a structure worth handing to a reader rather than only to a person. | `contracts/behaviour/.json`, and [`media-register.md`](./media-register.md) for when yours is the markup | ## How much of it you take **Three steps, and each one is a place to stop.** A project on the first step is using Arena correctly and is not waiting to finish; what the later steps buy is stated so you can decide against them on purpose rather than by not knowing. **The first step, take the language as it comes.** Install the package, write `arena.config.json` with one palette and the three font slots, leave `stylePlugins` out or set it to `default`, import the whole stylesheet, and compose with components. Put the air between them on containers of your own with the classes [`page.md`](./page.md) names, and paint the page through its role rather than a palette colour. This is the shortest honest route and the right one for a first screen, a prototype or a tool nobody outside the team will look at. **What it leaves unresolved is the appearance**: the product wears the answers Arena installs with, which are Dravensoft's, and every project on this step looks like every other one. **The second step, make it look like yours.** Write a style plugin, which is a directory holding one entry per role, and name it first in `stylePlugins`. Your palettes and your fonts go in the same config. From here the corners, the weights, the borders, the depth, the internal air and which colour each surface takes are the product's own, and no component is rewritten to get there. [`style-kernel.md`](./style-kernel.md) is where that decision is made and which answers carry the visible difference. **The third step, hold it.** Run the command's audit with `--strict` over the kinds you have not already decided against, so a raw value or a class of your own on an Arena component stops being something only a reader notices. Trim the stylesheet to what you render. Reach the metadata entry point if [`seo.md`](./seo.md) answered yes. Bind the pattern from `contracts/behaviour/` for any element you draw yourself, and take the export instead of the reimplementation wherever one exists. ## Where Arena stops Every line here is a decision rather than a gap waiting to close, so a project can plan around it instead of waiting for it. - **No page shape.** Arena ships the floor, the column, the air and the components, and no shape they must make. A console and a reading view are different pages before they are different components, so the composition is yours and the silence about it is deliberate. - **No `` in React.** That layer renders no title, description or canonical at all, and a React project carries its own through whatever its framework offers. - **No prerender tooling and no sitemap.** How an application is assembled is the project's answer rather than Arena's, and both layers are written for a server render; [`cold-start.md`](./cold-start.md)'s ninth node answers each architecture with the evidence behind it. What Arena ships no tooling for is producing the pages or listing them, and it writes into the head of whatever page your application renders. - **No gate reads your application.** The audit reads source text, and only for what source text can show. One primary accent per view, a filled danger surface, and every rule about meaning are held because you hold them and by nothing else. - **The component list ends at the register.** A photo wall, a feed of posts, a slideshow, a document editor and a game map are markup you write, and the skin still travels; [`media-register.md`](./media-register.md) is what Arena hands you instead of a component. - **No date picker and no time picker.** The native `type="date"`, `"time"` and `"datetime-local"` are the sanctioned controls: keyboard accessible, localized, and already known to whoever drives a phone. Arena's job is to make them look like Arena, which it does in both themes, and `ArenaInput`'s prompt in your layer is where they are written. - **No route into a component's own box.** No component takes a class or a style of yours, and the class names one renders are compiler output that no contract names. Your layout goes on a container you own. - **No icon font bundled.** Icons are Phosphor class-name strings a component renders, and the font arrives as a peer dependency rather than inside the package. - **No runtime dependency and no CSS toolchain.** Every component's CSS ships compiled against Arena's own class names and tokens, so a project running its own utility framework cannot collide with it in either direction. - **One pattern has no export.** The feed pattern is bound inside a component that is an event log with a fixed row, so it is not a feed of yours to reuse. Read how it binds the pattern and write the same thing around your own article. # The page you paint Arena paints no page of yours. It draws the components, declares the floor they stand on, and ships the column and the air as classes you put on your own markup, which leaves the page itself a thing you write and gives you every value to write it with. Read this before the first screen and again while you write one, because it is the half of a page that is never a component: what colour your own markup takes, how wide the content column gets, and how much space goes between one component and the next. ## The floor, and Arena does not paint it **`--fill-page` is the floor of the page**, and it is yours to apply. Put it on the element that owns the whole viewport, because a page with no floor shows the browser's own canvas below the first screenful, which is white under a dark palette and is the most common way a correctly built Arena screen looks broken. **What Arena does declare is `color-scheme`, from the palette's polarity.** The scrollbars, the native controls and the autofill the browser draws point the way the palette does without you asking, which is why a palette states whether it is dark or light rather than leaving it inferred. ## Which colour your own markup takes **Reach for a role when the thing you are drawing is furniture, and an alias when it is voice.** Both are legitimate, and the difference is what happens when the skin changes: a role is what a style plugin re-answers, so markup painted through one follows the appearance it is handed; an alias resolves to a palette colour and follows the palette instead. **Nineteen of the kernel's roles are colours**, and these are the ones a page of yours reaches for. Their full descriptions are one entry each in [`contracts/design/roles.json`](../../../contracts/design/roles.json). | what you are drawing | the role | |---|---| | the page itself | `--fill-page` | | a surface belonging to the page: a panel, a tile, a block of yours that reads as a card | `--fill-surface` | | a surface floating over it: a popover, a layer of your own | `--fill-surface-floating` | | a region recessed into a surface: a code block, a well | `--fill-surface-sunken` | | a box somebody types into | `--fill-field` | | what a row or a cell of yours takes under the pointer | `--fill-hover` | | the ground a value runs along, or the box a set of segments sits in | `--fill-track` | | a heading | `--ink-heading` | | text somebody reads, and the default answer for any text slot | `--ink-body` | | the small line above a title that says what kind of thing this is | `--ink-eyebrow` | | text held back: a caption, a hint, a timestamp | `--ink-muted` | | the line enclosing a surface, and the default ground line | `--edge-surface` | | the line around a floating surface | `--edge-surface-floating` | | the line around a control, a quiet control, a field, a marker | `--edge-control`, `--edge-control-quiet`, `--edge-field`, `--edge-marker` | | the rule dividing one thing from the next inside a surface | `--edge-separator` | | the wash a media frame draws under anything laid over it | `--overlay-media` | Each edge role has a width beside it, `--bw-surface`, `--bw-control`, `--bw-field`, `--bw-separator` and `--bw-marker`, so a border of yours is a role for the colour and a role for the thickness and never a length you chose. **A role says which colour the text takes, and a level says how far it is held back**, and text needs both. Under the default style plugin `--ink-muted` and `--ink-body` resolve to the same colour: what makes the held-back register held back is the level mixed into it, which is why a bare `var(--ink-muted)` paints a caption at the strength of body copy and nothing reports it. Spend one the way every Arena component spends it: ```css color: color-mix(in oklab, var(--ink-muted) var(--level-ink-muted), transparent); ``` `--level-ink-body`, `--level-ink-quiet` and `--level-ink-muted` are the three, in [`contracts/design/colors.css`](../../../contracts/design/colors.css). They are floors rather than constants, and `arena-to-prod` raises one for a palette whose ink has too little room to clear its contrast bar, so a percentage of your own is the one value on this page that cannot follow the palette it was written against. **The aliases are in [`contracts/design/colors.css`](../../../contracts/design/colors.css)**: `--crimson` and `--gold` for the two accents with a soft wash beside each, `--danger`, `--success`, `--warning` and `--info` for the four status colours with the same, and `--bone` and `--mute` for text at full strength and held back. The percentages the held-back registers stand at are tokens rather than numbers frozen into a rule, so raising one is a palette decision and not an edit in a hundred places. ## Meaning and identity are two different colour sets **A status colour means something and a ramp slot identifies something, and neither does the other's job.** This is the rule the components hold, and markup of yours holds it the same way or a reader learns that green is sometimes a category. - **Meaning** is the four status colours. `arenaToneColor(tone)` from your package resolves the one a tone stands for, so a shape you draw yourself keeps meaning what the components mean by it. - **Identity** is the eight ramp slots, `--color-cat-1` through `--color-cat-8`, in fixed order. The order is the identity, so slot three is slot three in every chart on the screen. `arenaCatColor(slot)` and `arenaCatTint(colour)` are there for a legend or a chip you draw. ## The column the page sits in `css/page.css` and `css/prose.css` carry four classes, and each goes on markup you wrote. - **`.arena-shell`** fills the window, so a short page's footer sits at the bottom rather than floating halfway up. - **`.arena-shell__main`** goes on the one child of the shell that should take the slack. It is a class rather than something the shell distributes because a shell with a header, a main and a footer has exactly one child that should grow and no rule can know which. **If the child that should grow is an Arena component, wrap it in a `
` of your own and put the class on the div.** Refusing to put a class on an Arena element is right and stopping there is not: a component's own element may declare `display: contents` and carry no box, so a shell whose growing child is a component and has no wrapper distributes its slack to nothing and the footer floats halfway up the page, which is the same failure the shell exists to prevent. - **`.arena-band`** centres its contents at the page width with a gutter either side. Put it inside anything that spans the viewport so the contents line up with the page above and below. - **`.arena-prose`** holds a reading column to a measure in `ch` rather than a pixel width, so it tracks the font size the way a measure has to. Put it on an article or a section you wrote. **All three lengths are roles**: `container-max`, `gutter` and `measure-prose`. A style plugin written for reading narrows the column, and every page you already shipped follows without an edit. Narrowing the column while keeping the gutter says the page is a document; widening both says it is a console. **The band carries the width and the gutter and no block air.** The space above and below a page's content is yours, and it is a `padding-block` on a container of your own spent on the `--sp-*` scale below. It is not a rhythm step: the classes in the next section answer the gap between two siblings, and this is the padding of the box that holds them, which is a different question with a different answer. **The gutter is a ceiling and not a fixed inset**, so the band stands off by the whole of it at or above the page width and by the same share of the space it has below that. A length answered for a page that reaches its full width is the wrong length on a phone, where a fixed inset either side leaves the content narrower than the air around it, and a class that only works at one width is a class a screen has to override. Nothing on your side answers this: the band already does it, and what you write is the same one class at every width. ## The air between two components **Arena draws no outer margin on anything**, so the space between one component and the next is always yours to place. `css/rhythm.css` is that half, as three named steps rather than a number you pick. | the class | the step | when | |---|---|---| | `.arena-stack` | `--rhythm-component` | a column of peers: a card and the next card, a chart and the table under it | | `.arena-stack--group` | `--rhythm-group` | a column of things that read as one unit: a label and its field, a card's own stacked children | | `.arena-stack--section` | `--rhythm-section` | between two sections of a page, which answer different questions | | `.arena-row` | `--rhythm-group` | two or more things side by side that read as one unit: a mark beside a name, a label beside its badge | | `.arena-row--component` | `--rhythm-component` | things side by side that are separate things: a bar's links, a toolbar's buttons | **The three lengths are also custom properties**, `--rhythm-group`, `--rhythm-component` and `--rhythm-section`, so a grid of your own or a rule the classes do not cover spends the same step rather than a fresh number. Reach for the class first: it carries the display and the direction with the gap, and a `gap` you write yourself is a rule that can drift off the step. **The miss this replaces has one shape, and it is small enough to look like nothing.** A column of your own carrying `display: flex`, `flex-direction: column` and a `gap`, holding a title over its identifier inside a table cell, or a label over the value under it, written inline because reaching for a class felt like more than two lines were worth. `.arena-stack--group` is exactly that block, and the step a group is spent at is the same step wherever it is spent. **The row is missed the same way and more often, because a short strip does not look like a layout.** A mark and the product's name in an app bar, two links beside each other, an icon and the word after it, a status label next to the badge it describes: each of those is a row, each is usually written as a `display: flex` with a gap somebody chose, and each is a step off the scale that no gate on your side reports. `.arena-row` is the horizontal half of the three steps above and it answers all of them, whatever the line holds and however short it is. That it wraps when the line runs out is a property it has, never the test for whether it applies: two elements side by side are already a row. **Five modifiers carry no length and line the items up instead**: `.arena-stack--start`, `.arena-stack--end`, `.arena-row--start`, `.arena-row--baseline` and `.arena-row--between`. They answer a question about your content, a trailing figure against a wrapping name, which is why they are here rather than in the kernel. **These classes go on an element you wrote, and they are useless on an Arena element.** A component's own element may declare `display: contents` and carry no box, and one component renders no element of its own at all, so an Arena element is never a layout target. **When the element you need to lay out is a component, the answer is a `
` of your own around it**, not dropping the class: refusing the class and writing no wrapper leaves the layout unstated, which looks like restraint and reads on the screen as a bug. **The air inside a component is not this.** `gap-control`, `gap-inline`, `gap-items`, `pad-surface`, `pad-control-x` and `pad-control-y` are kernel roles, and you move them by answering them differently in your style plugin rather than by writing a rule against a component. That is the whole cut: between is yours and comes from here, inside is the kernel's and comes from [`style-kernel.md`](./style-kernel.md). ## The rest of the scale, and the edges of the device **`--sp-*` is the spacing scale in thirteen steps**, and it is what everything the rhythm classes do not cover reads: `--sp-0` through `--sp-6` one step at a time, then `--sp-8`, `--sp-10`, `--sp-12`, `--sp-16`, `--sp-20` and `--sp-24`. A bare length is a bug and this is the scale that makes it unnecessary. [`contracts/design/Scales.md`](../../../contracts/design/Scales.md) says what each step is for. **`--pad-safe-top`, `--pad-safe-right`, `--pad-safe-bottom` and `--pad-safe-left`** compose the device's own insets with that scale, in [`contracts/design/environment.css`](../../../contracts/design/environment.css). Use them on a shell you draw around Arena, so a bar pinned to the bottom of a phone screen clears the home indicator without you measuring one. **Two more classes are for markup rather than components.** `.arena-num` from `css/numerals.css` puts a figure on the mono face with tabular figures and no colour, so a column of them aligns by digit and does not jitter as it counts. `.arena-sr-only` from `css/sr-only.css` is a label a screen reader announces and nothing paints, which is where the name of an icon-only control you drew yourself goes. ## Taking the least of this, and taking all of it **The least is the classes and the roles as they stand.** Paint the floor with `--fill-page`, your text with `--ink-body` and `--ink-muted`, your own surfaces with `--fill-surface` and `--edge-surface`, put `.arena-shell` and `.arena-band` around the page and `.arena-stack` between things. Nothing here needs a style plugin, and a screen built this way already follows whatever appearance the project adopts later. **All of it is the same page with the roles re-answered.** Every name above that is a role rather than an alias is a question your own style plugin answers, so the page you already wrote takes the new corners, the new borders, the new column width and the new air without one of its rules being edited. That is the reason to reach for the role over the alias in the first place, and [`style-kernel.md`](./style-kernel.md) is where the answers are written. # Arena components Every component Arena ships, by the category it is filed under. **This page answers one question: whether a component exists at all, and which layers ship it.** It is not a stop on the way to writing one: what a member is called where you are building, and how to write it, is your own layer's index and then the component's own prompt, and reading this page first buys nothing when you already know what you are reaching for. **The rules of the language every component below answers to are stated in [`../skills/design/SKILL.md`](../skills/design/SKILL.md) before any component on this page.** Nothing here restates them, so a screen built from this page alone breaks the rules where nothing will report it. | Layer | Index | Package | |---|---|---| | Angular | [`angular/INDEX.md`](./angular/INDEX.md) | `@dravensoft/arena-angular` | | React | [`react/INDEX.md`](./react/INDEX.md) | `@dravensoft/arena-react` | - **Takes** is the members its API contract declares, in contract order, under the neutral names the contract gives them. A member marked `*` is required. Your layer's index gives the name each one binds to there, and the component's own prompt gives its type and its default. - **Behaviour** is the accessibility pattern the component binds. `none` means no pattern applies, which is a claim the binding argues rather than an omission. - **Layers** is which layers ship the component today. ## brand | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaAppLogo` | Brand lock-up: a mark beside or above a product name. | `mark*` `name*` `dim` `size` `orientation` | none | angular, react | ## charts | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaBarChart` | Categorical bars on one axis. Dependency-free SVG that reads the token layer directly, with a visually-hidden table of the same numbers. | `labels*` `series*` `label*` `stack` `valueSuffix` `valuePrefix` `valueFormat` `height` `minPointSpacing` | figure-with-data-table | angular, react | | `ArenaChartCard` | A titled card frame around a chart, with an optional actions slot in its head. | `title` `headingLevel` `actions` `content` | none | angular, react | | `ArenaDoughnutChart` | Parts of one whole, as a ring with a legend beside it. Identity only: a slice is a category by definition, so there is no tone. Dependency-free SVG with a visually-hidden table of the same numbers. | `labels*` `series*` `label*` `valueSuffix` `valuePrefix` `shape` `legendLayout` `sliceActivate` `valueFormat` | figure-with-data-table | angular, react | | `ArenaHorizontalBarChart` | Categorical bars running across the plot, with the categories down its left edge. The same data and the same table as ArenaBarChart with the axes transposed, which is a different component rather than an orientation flag because transposing changes what four of its members mean: the cursor answers the vertical arrows, the category gutter is its own token, height becomes the axis the data grows along, and there is no scrolling rail. | `labels*` `series*` `label*` `stack` `valueSuffix` `valuePrefix` `valueFormat` `height` | figure-with-data-table | angular, react | | `ArenaLineChart` | One series over an ordered sequence, on one axis. Dependency-free SVG with a crosshair that snaps to the nearest point, and a visually-hidden table of the same numbers. | `labels*` `series*` `label*` `area` `curve` `valueSuffix` `valuePrefix` `valueFormat` `height` `minPointSpacing` | figure-with-data-table | angular, react | | `ArenaPyramidChart` | A population pyramid: two counts per band, mirrored about a shared centre line, with the band names down the left edge. Diverging from a centre is the whole point rather than an option, which is why the two-series rule and the mirrored axis are the component instead of a flag on a bar chart. Dependency-free SVG with a visually-hidden table of the same numbers. | `labels*` `series*` `label*` `valueSuffix` `valuePrefix` `valueFormat` `height` | figure-with-data-table | angular, react | | `ArenaRadarChart` | Several measures on one shape: an axis per label around a polar grid, one closed polygon per series. Reads a profile rather than a magnitude, which is what makes it a different chart from bars over the same numbers. Dependency-free SVG with a visually-hidden table of the same numbers. | `labels*` `series*` `label*` `fill` `valueSuffix` `valuePrefix` `valueFormat` `height` | figure-with-data-table | angular, react | | `ArenaScatterChart` | Two quantities against each other, one mark per pair, and a third as the mark's size when a series carries one. The first chart here whose horizontal axis carries a value rather than a position, which is why it takes a series of parallel measurements and names every axis it draws. Dependency-free SVG with a visually-hidden table of the same numbers. | `series*` `label*` `xLabel*` `yLabel*` `sizeLabel` `sizeLegend` `valueSuffix` `valuePrefix` `valueFormat` `height` | figure-with-data-table | angular, react | ## display | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaActivityFeed` | An event feed: someone did something to something, then. Arena draws every row. | `label*` `items*` `busy` | feed | angular, react | | `ArenaAvatar` | A person or entity mark: the image when `src` is set, initials from `name` otherwise, with an optional presence dot. | `src` `name` `size` `shape` `status` | none | angular, react | | `ArenaBadge` | Status label: mono, uppercase, short. Carries an object's actual state or an editorial emphasis, never decoration. | `content` `tone` `dot` | none | angular, react | | `ArenaCalendar` | Week or day schedule on a time grid. Colour is identity, never state. | `content` `timeZone` `anchorDate` `view` `dayStart` `dayEnd` `weekStartsOn` `hideEmptyWeekend` `dayInteractive` `dateClick` `rangeChange` `actions` | grid | angular, react | | `ArenaCalendarEvent` | One event on an ArenaCalendar's schedule. Times are ISO datetimes read in the calendar's timeZone, never the reader's. ArenaCalendar draws the chip; the consumer writes one of these per event and ArenaCalendar settles where it goes. | `id*` `title*` `start*` `end*` `colorId` `interactive` `actionsEnabled` `actions` `disabled` `click` | button, none | angular, react | | `ArenaCard` | Surface container. Hairline border on the base surface scale; depth comes from the shadow, never a gradient. | `content` `interactive` `disabled` `href` `action` `title` `headingLevel` `eyebrow` `floating` `accent` `click` | button, none | angular, react | | `ArenaKeyValue` | A list of terms and the values against them, with an optional summed row ruled off at the bottom: a basket summary, an order, an invoice, a panel of facts about a resource. It renders a real definition list, so the association between a term and its value is the platform's rather than a class name's. | `rows*` `total` | none | angular, react | | `ArenaPeopleList` | A list of people, each row a face with a name and, where the list is about a quantity, a figure beside it. Standings, assignees, members, suggestions. The list owns the semantics and the row size; a row owns what it says. | `label*` `ordered` `size` `content` | none | angular, react | | `ArenaPersonRow` | One person in an ArenaPeopleList: a face, a name, an optional line under it, an optional position in front and an optional figure behind. Its size comes from the list rather than from here. | `name*` `src` `secondary` `rank` `figure` `current` `action` | none | angular, react | | `ArenaSkeleton` | A loading placeholder that reserves the space real content will take. | `variant` `width` `height` `lines` `radius` | status | angular, react | | `ArenaStatCard` | One metric on a card surface: a micro-label, the number, an optional delta pill and a sub-line. | `label*` `value*` `tone` `delta` `sub` `icon` | none | angular, react | | `ArenaTable` | Data table on the density tokens. ArenaTable draws the header row from `columns`, owns the grid and its keyboard, and decides where each row sits; the consumer writes one ArenaTableRow per row and one ArenaTableCell per cell, so a cell's content is a value or one of Arena's own components rather than something returned from a per-item render function. Below --bp-md it becomes one card per row, measured on its own container rather than the viewport. | `label*` `columns*` `content` `empty` `sort` `sortChange` `page` `slice` `pageChange` `pageControl` `sortControl` `responsive` | grid, none | angular, react | | `ArenaTableCell` | One cell of an ArenaTableRow. It draws the cell box (the padding, the alignment and the mono/gold treatment its column asks for, and in card mode the label/value pair or the full-width block), and shows whatever the consumer put in it. Its column config, its layout and its place in the grid's keyboard order come from ArenaTable and ArenaTableRow and are members of no contract. | `content` `href` `navigate` | none | angular, react | | `ArenaTableRow` | One row of an ArenaTable. The consumer writes one per row and one ArenaTableCell inside it per cell. Where the row sits, the columns its cells are set against and how the keyboard reaches them are ArenaTable's, not this component's, and are members of no contract: the same shape, and for the same reason, as an ArenaRadioGroup and its Radios sharing which one is checked. | `content` `interactive` `disabled` `click` | button, none | angular, react | | `ArenaTag` | A pill for filters, technologies and statuses. A tone for what state a thing is in or a ramp slot for which thing it is, and an optional dismiss. | `content` `tone` `colorId` `removable` `disabled` `remove` | button, none | angular, react | | `ArenaUnauthCard` | The panel a signed-out screen needs: sign in, check your inbox, this link expired, a two-factor code. It knows nothing about credentials on purpose; the fields are composed inside it. | `brand` `eyebrow` `title` `headingLevel` `content` `footer` | none | angular, react | ## feedback | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaAlert` | A tone-coloured message with an optional icon, a single action, and optional dismissal. | `tone` `title` `content` `icon` `actionLabel` `action` `dismissible` `close` | alert, status | angular, react | | `ArenaConfirmDialog` | Confirmation of a high-consequence action. Never closes on click-outside. `requireText` locks the confirm button until a word is typed. | `open*` `title*` `eyebrow` `content` `confirmLabel` `cancelLabel` `destructive` `requireText` `cancel` `confirm` | alertdialog | angular, react | | `ArenaDialog` | Modal dialog over a blurred scrim. Takes the whole interaction until dismissed. | `open*` `title*` `eyebrow` `width` `content` `footer` `close` | dialog-modal | angular, react | | `ArenaEmptyState` | A placeholder for an empty collection: an icon, a title, a message, and an optional action. | `icon` `title*` `headingLevel` `message` `action` | none | angular, react | | `ArenaErrorState` | Section/screen-level failure, with recovery and an optional diagnostic code. | `icon` `title` `headingLevel` `message` `code` `retryLabel` `retry` `secondaryAction` | alert | angular, react | | `ArenaOnboarding` | Guided coachmark tour (H10): presents features within the product with progress dots, Skip and Next. Controlled: the host owns index and answers the four events. | `open*` `steps*` `index` `anchor` `next` `back` `skip` `done` | dialog-modal | angular, react | | `ArenaProgressBar` | Determinate progress by default; indeterminate for a wait with no percentage. A bar along a row, or a ring with the figure inside it. | `content` `progressPercentage` `indeterminate` `tone` `label*` `showLabel` `showPercentage` `size` `shape` | progressbar | angular, react | | `ArenaSheet` | A non-modal panel anchored to one edge of the page: a cart, a filter drawer, a detail pane. It carries no scrim, traps no focus and takes nothing away from the page behind it, which is the whole difference from a dialog. Its header stays on screen while its body folds away, so a reader keeps the panel without keeping its bulk. | `open*` `placement` `title*` `collapsed` `collapsedChange` `dismissible` `close` `content` `footer` | disclosure | angular, react | | `ArenaSpinner` | Indeterminate wait indicator. For a measurable process use ArenaProgressBar instead. | `size` `tone` `label` | progressbar | angular, react | | `ArenaToast` | Ephemeral notification with a tone-coloured side bar and one optional action. | `title` `message` `tone` `actionLabel` `action` `persist` `dismissible` `close` | alert, status | angular, react | | `ArenaToastHost` | The fixed box a stack of notices renders into. It decides where the stack sits, how far it stands off the viewport edges and how much air separates two notices, and it decides nothing else: it reads no notice, counts none, and owns no clock. | `placement` `content` | none | angular, react | | `ArenaTooltip` | A short label revealed on pointer intent. Bone over dark for contrast. It waits before appearing and before withdrawing, so a pointer crossing a toolbar reveals nothing. | `label*` `content*` | tooltip | angular, react | ## forms | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaButton` | Action button. One primary per view; danger stays outline. | `content` `variant` `size` `icon` `iconRight` `loading` `full` `disabled` `type` `name` `value` `autoFocus` `form` `tabStop` `click` | button | angular, react | | `ArenaCheckbox` | A single checkbox. Checked shows a crimson fill with a check. | `checked` `label` `disabled` `required` `name` `value` `change` | checkbox | angular, react | | `ArenaIconButton` | Icon-only button. Carries an accessible name in every state, not only on hover. | `icon*` `label*` `size` `variant` `showLabel` `pressed` `disabled` `type` `name` `value` `autoFocus` `form` `tabStop` `click` | button | angular, react | | `ArenaInput` | Text field with validation. Focus is a gold ring; error crimson; valid green with a check. The four states are ordered and the order is normative: error, then focus, then valid, then neutral: an errored field stays crimson while it has focus, because the validation signal must not disappear at the moment the user acts on it. | `label` `id` `hint` `error` `valid` `required` `validate` `validateOn` `type` `icon` `prefix` `value` `disabled` `readOnly` `placeholder` `name` `autoComplete` `min` `max` `step` `maxLength` `pattern` `change` `blur` | textbox | angular, react | | `ArenaRadio` | One option inside an ArenaRadioGroup. Selected shows a crimson dot inside the ring. | `value*` `label` `hint` `disabled` | radiogroup | angular, react | | `ArenaRadioGroup` | Single-selection group. Governs the value and distributes it to its child Radios. | `ariaLabel*` `content` `value` `name` `change` | radiogroup | angular, react | | `ArenaSelect` | Styled native dropdown selector, with the same validation vocabulary ArenaInput carries. The four states are ordered and the order is the same normative one: error, then focus, then valid, then neutral -- an errored control stays crimson while it has focus, because the validation signal must not disappear at the moment the user acts on it. A form that mixes ArenaInput and ArenaSelect is a form whose fields must report a failure the same way, or it gets validated by hand or not at all. | `label` `placeholder` `options` `value` `disabled` `required` `hint` `error` `valid` `icon` `name` `change` | select | angular, react | | `ArenaSwitch` | A controlled on/off switch showing an icon per state. `confirm` gates a high-impact change through an ArenaConfirmDialog before it applies. | `state` `orientation` `size` `iconOn` `iconOff` `label*` `disabled` `confirm` `funcOn` `funcOff` `requestChange` | switch | angular, react | | `ArenaTextarea` | Multi-line text field with validation and an optional counter. | `label` `id` `hint` `error` `required` `counter` `autoResize` `value` `disabled` `readOnly` `placeholder` `name` `maxLength` `rows` `change` | textbox | angular, react | ## layout | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaBoard` | The frame of a board: columns side by side, as wide as the room allows and never narrower than one column's minimum, scrolling sideways when they no longer fit. It is one tab stop carrying a group role and a name, which is what a scrolling region needs to be reachable by keyboard at all. It draws no card and moves nothing: what a column holds is the consumer's, and reordering is theirs too. | `label*` `content*` `minColumn` | scrollable-region | angular, react | | `ArenaBoardColumn` | One column of an ArenaBoard: a named head with a count and a control, a stack of whatever the consumer puts in it, and a footer for the one action that adds to it. The cards are the consumer's own, because a card on a board carries that product's fields and nothing general is left once they are removed. | `title*` `headingLevel` `count` `summary` `colorId` `action` `content` `footer` | none | angular, react | | `ArenaFigure` | A framed piece of media with an optional caption: an image, a video, or a stand-in for the one that has not arrived. The frame is a shape and a corner a style plugin answers, and it clips whatever is put in it, so a wall of figures reads as a wall rather than as whatever sizes the pictures happened to be. | `media` `fallback` `overlay` `caption` `ratio` | none | angular, react | | `ArenaGrid` | A grid that decides its own column count from the room it is given, rather than from a breakpoint anyone had to pick. Cells are as wide as they can be at or above a minimum, and the count falls as the room does, all the way to one. | `min` `gap` `maxWidth` `content` | none | angular, react | | `ArenaHero` | The opening of a landing page: one line the page is built around, what sits above and below it, the actions it asks for, and a figure beside or behind it. Its title takes the hero register, which is the top rung of the title ladder and the only one above the page head. | `title*` `headingLevel` `eyebrow` `lede` `actions` `figure` `layout` `align` | none | angular, react | | `ArenaMain` | The page's main landmark, and nothing else. Arena requires a name on both of its nav bars because what a nav is FOR is editorial, and then shipped no way to say where the content itself begins, which is the one region of a page it had not kept its own decision about. This draws no box: no display of its own beyond the block a landmark already is, no width, no grid, no padding. Where things go stays the container the consumer owns, and this says only what the region IS. It is also the target an ArenaSkipLink jumps to, so it is focusable programmatically: an anchor pointing at a container the platform will not focus scrolls the page and leaves focus behind, which is the failure that makes most skip links look like they work. | `content` | main | angular, react | | `ArenaScroller` | A row that scrolls sideways because it holds more than fits, with no arrows pretending to be a slideshow. It is one tab stop carrying a group role and a name, which is what a scrolling region needs to be reachable by keyboard at all: without it, everything past the right edge belongs to the pointer alone. Each child is laid out at one width so the row reads as a rail rather than as a line of whatever the children happened to measure. | `label*` `content*` `itemWidth` `behaviour` | scrollable-region | angular, react | | `ArenaScrollerItem` | One cell of an ArenaScroller: the box that carries the width the row decided and the point the row settles on. It exists because a row cannot reach inside its children to size them, and a child that is an Arena component may render no box of its own at all, so a rule aimed at the row's direct children lands on nothing in one layer and on the card in the other. The item is the box both layers agree about. | `content` | none | angular, react | | `ArenaSection` | A named region of a page: a heading, optionally an eyebrow above it, a line under it and an action beside it, over whatever the region holds. It wraps what the consumer wrote and never replaces it. The title register is the section one, a step above a card's and a step below a page's, so a style plugin re-pitching the hierarchy moves this with the other two rather than leaving a page with three heads that disagree. | `title*` `headingLevel` `content*` `eyebrow` `description` `action` `rhythm` | none | angular, react | | `ArenaSiteFooter` | The band across the bottom of every screen: what a page says about itself once it has finished. It is the contentinfo landmark, so there is one per page, and like the bar at the top its contents line up with the page above them. | `content` `note` | contentinfo | angular, react | | `ArenaSkipLink` | The link that lets a keyboard reader step over everything repeated on every screen and land in the content. It is four details and almost nobody gets all four: it has to be the first focusable thing in the document, invisible until it takes focus and visible the moment it does, and its target has to be focusable programmatically or focus never moves. Arena owns all four, and the consumer owns only the words. It points at the ArenaMain on the page, by an id Arena writes on both sides, so nothing is coordinated at the call site: a page has one main landmark, which is what makes a fixed id right here and wrong almost everywhere else. Place it first in the shell, above the header and the nav bars it exists to skip, which is the one thing it cannot do for itself. | `label*` | none | angular, react | ## navigation | Component | What it is | Takes | Behaviour | Layers | |---|---|---|---|---| | `ArenaAppBar` | The band across the top of every screen: the site's identity, the way through it, and the controls that follow the reader everywhere. It is the banner landmark, so there is one per page, and its contents line up with the page under it because the bar spans the viewport and the band inside it does not. | `brand` `nav` `actions` `sticky` | banner | angular, react | | `ArenaBottomNav` | The bar of destinations pinned to the bottom edge of a phone screen. A compound component: the consumer writes one ArenaBottomNavItem per destination, and the coordination that tells each child which id is active and how to report `nav` is the parent's. That coordination is a member of no contract and each layer wires it in its own idiom. It is a row of equal columns with the glyph above the label, which is what separates it from a sidebar's stack of indented rows. | `active` `ariaLabel*` `content` `nav` | navigation | angular, react | | `ArenaBottomNavItem` | One destination in an ArenaBottomNav. The consumer writes one per destination; which id is currently active and how it reports `nav` are settled between it and its parent, and none of that is a member of this contract, the same shape and the same reason as ArenaSideNavItem and as the name an ArenaRadioGroup settles with each ArenaRadio. It draws its glyph above its label and takes an equal share of the bar's width, however many destinations there are. | `id*` `label*` `icon*` `badge` `href` `disabled` | button, none | angular, react | | `ArenaBreadcrumbs` | A trail of ancestor locations ending at the current one. An explicit return path for hierarchies deeper than tabs. | `ariaLabel*` `items*` `separator` `origin` `navigate` | navigation | angular, react | | `ArenaBulkActionBar` | Appears when rows are selected and operates on the selection as a set. Renders nothing at a count of zero. | `count*` `noun` `actions*` `run` `layout` `clearable` `clear` | toolbar | angular, react | | `ArenaCommandPalette` | Power-user accelerator (Cmd/Ctrl+K): search and run actions without a mouse. Controlled: the host owns whether it is open. | `open*` `commands*` `placeholder` `maxResults` `close` `run` | combobox | angular, react | | `ArenaMenu` | Dropdown menu of actions on a trigger -- overflow, more actions, context. | `trigger*` `items*` `align` `select` | menu-button | angular, react | | `ArenaPageHead` | A page heading: a required title, an optional subtitle, and an optional actions slot. | `title*` `headingLevel` `subtitle` `actions` `align` | none | angular, react | | `ArenaPagination` | Page selector for a paged list. Renders a windowed range, never every page. | `page*` `pageCount*` `ariaLabel*` `change` | navigation | angular, react | | `ArenaSegmentedControl` | A compact inline filter over mutually exclusive options. A real radio group, never a tab list, and it carries no crimson. | `options*` `value` `defaultValue` `size` `ariaLabel*` `name` `change` | radiogroup | angular, react | | `ArenaSideNav` | The sidebar's navigation list -- the list alone, not the frame around it. A compound component: the consumer writes one ArenaSideNavItem per destination, and the coordination that tells each child where it sits, which id is active and how to report `nav` is the parent's. That coordination is a member of no contract and each layer wires it in its own idiom. | `active` `ariaLabel*` `content` `indentStep` `nav` | navigation | angular, react | | `ArenaSideNavCollapsible` | A named group inside an ArenaSideNav that shows and hides its own contents. It may contain items, sections and further collapsibles to any depth; the indent compounds with depth, which is why ArenaSideNav.indentStep is a step rather than a total. It binds the `disclosure` pattern and deliberately does NOT claim to be a treeview: there is no aria-level, no roving tab stop and no arrow navigation, because each collapsible is an independent disclosure. Its expanded state lives in the component, seeded by defaultExpanded, and it additionally opens itself when it COMES TO HOLD ArenaSideNav.active -- a transition, not a standing condition, which is what leaves a user free to collapse a group again while the route stays inside it. Implicit behaviour, stated here and in its .prompt.md rather than left to be discovered. | `id*` `label*` `icon` `defaultExpanded` `content` `toggle` | disclosure | angular, react | | `ArenaSideNavItem` | One destination in an ArenaSideNav. The consumer writes one per destination; the nesting depth it sits at, which id is currently active and how it reports `nav` are settled between it and its parent, and none of that is a member of this contract -- the same shape, and the same reason, as the name, checked state and selection callback an ArenaRadioGroup settles with each ArenaRadio. It used to be api/types/side-nav-item.json, a predefined object Arena read out of an array; it is an element the consumer writes now, which is what makes a section, a collapsible and arbitrary nesting expressible at all. | `id*` `label*` `icon` `disabled` `badge` `href` | button, none | angular, react | | `ArenaSideNavSection` | A named group of navigation items inside an ArenaSideNav. It wraps what the consumer wrote and never replaces it; its accessible name is the same heading a sighted user reads. A section always has children -- a childless one is guarded against at runtime, because two shapes would be one more thing a single behaviour binding cannot describe. Having sections at all is optional: loose SideNavItems at the root are legal and may sit beside them. | `label*` `content*` | none | angular, react | | `ArenaTab` | One tab in an ArenaTabs strip, and the panel it shows. ArenaTab draws the button; its content fills the tabpanel ArenaTabs renders beside the tablist. | `value*` `label*` `content` | none | angular, react | | `ArenaTabs` | A row of tabs and the one panel they switch between. Write one `ArenaTab` per view; ArenaTabs renders the tablist, the panel, and the keyboard. | `content` `value` `defaultValue` `change` | tabs | angular, react | 73 components across 7 categories. # Arena components, the Angular layer Every component this layer ships, under the category it is filed under. **This page is a directory, not an index.** Find the name you are reaching for in the middle column, open that category's index for what the component takes, and its own prompt for how to write it. The components are named here rather than described so that finding yours costs one page instead of a guess. **Read [`../../skills/design/SKILL.md`](../../skills/design/SKILL.md) before you write anything from here.** It carries the one thing no page below it does: the rules of the language, which hold in your code because you hold them and which no gate reads your application to enforce. A screen built from this tree alone breaks them where nothing will report it. - Installing the package, declaring your skin, and what it exports besides components: [`PACKAGE.md`](./PACKAGE.md). - Whether a component exists at all, including any this layer does not ship: [`../INDEX.md`](../INDEX.md). | Category | The components it holds | Index | |---|---|---| | `brand` | `ArenaAppLogo` | [`components/brand/INDEX.md`](./components/brand/INDEX.md) | | `charts` | `ArenaBarChart` `ArenaChartCard` `ArenaDoughnutChart` `ArenaHorizontalBarChart` `ArenaLineChart` `ArenaPyramidChart` `ArenaRadarChart` `ArenaScatterChart` | [`components/charts/INDEX.md`](./components/charts/INDEX.md) | | `display` | `ArenaActivityFeed` `ArenaAvatar` `ArenaBadge` `ArenaCalendar` `ArenaCalendarEvent` `ArenaCard` `ArenaKeyValue` `ArenaPeopleList` `ArenaPersonRow` `ArenaSkeleton` `ArenaStatCard` `ArenaTable` `ArenaTableCell` `ArenaTableRow` `ArenaTag` `ArenaUnauthCard` | [`components/display/INDEX.md`](./components/display/INDEX.md) | | `feedback` | `ArenaAlert` `ArenaConfirmDialog` `ArenaDialog` `ArenaEmptyState` `ArenaErrorState` `ArenaOnboarding` `ArenaProgressBar` `ArenaSheet` `ArenaSpinner` `ArenaToast` `ArenaToastHost` `ArenaTooltip` | [`components/feedback/INDEX.md`](./components/feedback/INDEX.md) | | `forms` | `ArenaButton` `ArenaCheckbox` `ArenaIconButton` `ArenaInput` `ArenaRadio` `ArenaRadioGroup` `ArenaSelect` `ArenaSwitch` `ArenaTextarea` | [`components/forms/INDEX.md`](./components/forms/INDEX.md) | | `layout` | `ArenaBoard` `ArenaBoardColumn` `ArenaFigure` `ArenaGrid` `ArenaHero` `ArenaMain` `ArenaScroller` `ArenaScrollerItem` `ArenaSection` `ArenaSiteFooter` `ArenaSkipLink` | [`components/layout/INDEX.md`](./components/layout/INDEX.md) | | `navigation` | `ArenaAppBar` `ArenaBottomNav` `ArenaBottomNavItem` `ArenaBreadcrumbs` `ArenaBulkActionBar` `ArenaCommandPalette` `ArenaMenu` `ArenaPageHead` `ArenaPagination` `ArenaSegmentedControl` `ArenaSideNav` `ArenaSideNavCollapsible` `ArenaSideNavItem` `ArenaSideNavSection` `ArenaTab` `ArenaTabs` | [`components/navigation/INDEX.md`](./components/navigation/INDEX.md) | 73 components across 7 categories in this layer. # @dravensoft/arena-angular [![npm](https://img.shields.io/npm/v/@dravensoft/arena-angular?style=flat-square&color=c5a059&label=npm)](https://www.npmjs.com/package/@dravensoft/arena-angular) [![downloads](https://img.shields.io/npm/dm/@dravensoft/arena-angular?style=flat-square&color=c5a059)](https://www.npmjs.com/package/@dravensoft/arena-angular) [![license](https://img.shields.io/npm/l/@dravensoft/arena-angular?style=flat-square&color=c5a059)](https://github.com/dravensoft-dev/arena/blob/main/LICENSE) ![The same Angular component under three style plugins, with the contracts that decide what it is](https://arena.dravensoft.org/hero.png) Arena is Dravensoft's design system. This package is its Angular layer: 73 components, standalone and `OnPush`, with signal inputs and outputs, styled by a shared Tailwind recipe per component, and shipped in Angular Package Format. **The package carries the language. It does not carry a skin.** Your palettes and your fonts are yours, declared in one JSON file, and the `arena-to-prod` command that ships here turns that file into the stylesheets Arena reads. **What comes down with it.** `@angular/core`, `@angular/common` and `@angular/platform-browser`, which you already have; `@angular/cdk`, because a primitive that anchors a surface to a trigger uses its overlay, and `arena-tabs` its roving-focus key manager to position themselves, and for position only, since the roles, the keys and the focus are Arena's own; and `@phosphor-icons/web`, which you may not have: Arena's icons are Phosphor class names a component renders, never SVGs it bundles, so the font is installed alongside the package rather than bundled inside it. `tslib` is the only runtime dependency this package declares. **You do not need to run Tailwind, and running your own cannot collide with this one.** Every component's CSS ships compiled, so one `@import` is enough and you compile nothing. The rules are written against Arena's own class names and read Arena's own tokens: Tailwind is how they were authored and nothing more, so a `--spacing` of your own moves nothing here. ## What Arena gives a project **Components whose API is a contract.** Every member of every component is declared once, in one place both layers are generated from and held to, so a name, a type, a default and what it means are the same under either framework, and the table documenting a component is emitted from that declaration rather than written beside it. **Behaviour each component binds rather than an audit somebody runs.** A component names the accessibility pattern it implements, most of them a [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) one: the roles it carries, the keys it answers, where focus goes and what dismisses it. The pattern is recorded per component, and so is anything a component does not yet meet, with its reason. **A style kernel, which is how Arena stops looking like Arena.** Arena keeps the questions about shape, space, weight and depth and ships one set of answers; a project writes its own as a style plugin, which is the ordinary case rather than an escape hatch. A palette is not an appearance, so the config below is the smaller half of that decision. **Every member documents itself where you already are.** The members of every component, each with the paragraph saying what it is for, ship as this package's type declarations, so an editor shows them on hover and the file the `types` entry resolves to is the same reference in one place. That is the member-level answer; what it does not carry is the language above it, which is the next section. **What a machine reads off a screen is part of what a component draws.** `ArenaBreadcrumbs` describes the trail it draws in `schema.org` terms, so a crawler reads the same trail a person does. Whether this package also writes your `` is a thing only this package can answer, and it answers it below. **A `` for a product that has to be found from outside it, and it is optional.** This package writes one, at `@dravensoft/arena-angular/metadata`: a title composed with Angular's own, a description, a canonical and the `og:*` pair, and every route private until it says otherwise. It sits behind a second entry point because reaching it means reaching the router, an optional peer nothing else here asks for. ## The screens it is for Arena is a **product-application library**: its components are the furniture of an application somebody works in, tables and their rows, forms and their fields, navigation, dialogs, charts, cards and the empty, loading and error states around them. A media or a consumer product is a different register, so a photo wall, a story ring, a feed of posts, a slideshow, a document editor and a game map are markup you write yourself. **The skin travels either way.** A style plugin answers every role whatever the product is, so what Arena does draw wears your appearance rather than Arena's. What runs out first is the component list. **When the answer is that the markup is yours, the pattern is still not.** This package ships `contracts/behaviour/`, one file per accessibility pattern, stating normatively what markup of yours has to do to carry it: the roles it takes, the keys it answers, where focus goes on open, what it returns to on close and what dismisses it. `dialog-modal.json` is the one a lightbox or a viewer of yours binds; `feed.json` a scrolling list of posts. Read the file for the pattern you are drawing, and take the exported helper named in the table below rather than writing a second copy of either half. ## This package ships the components and not the language See it before you install it: **https://arena.dravensoft.org** has the guidelines, a kitchen sink and a playground for every component. Source and full documentation: **https://github.com/dravensoft-dev/arena** **An agent building with Arena needs the repository, and this is a prerequisite rather than a recommendation.** The rules every component answers to, the usage document of each one, and the style kernel a project answers to make Arena look like its own product are all there and none of them is in this tarball. Install the **Claude Code plugin**, or clone the repository and point your agent at `skills/design/SKILL.md`, before you ask for the first screen. Over HTTP the same route starts at **https://arena.dravensoft.org/llms.txt**. Without them an agent guesses, and **no gate reads your application**, so nothing reports the difference: the screen renders, and the rules it breaks are the ones only a reader notices. The package is the code. The repository is the language. ## Install ```bash bun add @dravensoft/arena-angular # or: npm i / pnpm add ``` **npm, bun and pnpm all install this**, and pnpm is the one worth naming because its layout is the strict one: nothing here is hoisted into place by accident, since every peer is declared rather than assumed, and the command below resolves the icon font through the symlinked store like any other dependency. No hoisting flag is needed under any of the three. That is the whole install. Angular, the CDK and `@phosphor-icons/web` are peer dependencies, so your package manager brings down whichever of them the project does not already have. **The package declares `engines: { node: ">=22" }`**, and Angular's own floor is the stricter of the two, so it is the one that decides which Node your project runs. Arena never refuses a Node your framework accepts: the command it ships is plain JavaScript over `node:fs`, `node:path` and `node:url`, and a higher floor here would buy an engine warning on every install and nothing else. **An icon is a class name, not an element.** Every `icon` input takes a Phosphor class list, `"ph-bold ph-bell"`, and the component renders it. The stylesheet that turns those classes into glyphs is not the one Phosphor ships: it is the subset `arena-to-prod` writes for you, below. ## What this works with **Arena is built with bun, and bun is required nowhere.** The command below is a Node program reading three node modules, so whatever runs a Node program runs it. These are the two axes that decide whether this package fits a project at all; the rest of the repertoire, the bundler, the stylesheet route and the framework versions, each carrying the evidence behind it, is at [the repertoire page](https://arena.dravensoft.org/skills/design/references/stack.md). **What runs the command?** | You may choose | Evidence | What is true | |---|---|---| | Node >=22 | held by a gate | the command is a Node program, and that floor is the oldest line Node still supports rather than a capability the command needs | | any runner that reaches a Node program | allowed by the manifest, not exercised | npx and bunx both reach the same file. No runner is required: the command imports three node modules and no runtime API of its own | **How is the package loaded?** | You may choose | Evidence | What is true | |---|---|---| | an ES module import | held by a gate | the package is a module, and every target its exports name resolves to a file that is there | | a CommonJS require | does not work | there is no require condition and no second build. A project that cannot load a module cannot load this one, and it fails at the first import rather than somewhere later | ## One name everywhere Every element carries the `arena-` prefix and every exported class carries `Arena`, which are the same name in two spellings: `` is the element, `ArenaButton` is the class you put in an `imports` array, and `kebab('ArenaButton')` is what turns one into the other. ```ts import { ArenaButton, ArenaCard } from '@dravensoft/arena-angular'; @Component({ imports: [ArenaButton, ArenaCard], template: `Save` }) ``` Every type carries it as well, so a tone is an `ArenaTone`, and the projection markers carry it already: `ArenaAction`, `ArenaActions`, `ArenaBrand`, `ArenaFooter`, `ArenaSecondaryAction`, `ArenaFigureSlot`, `ArenaMedia`, `ArenaFallback`, `ArenaOverlay` and `ArenaNav`. **The class names carry it too, and they carry it once.** A component renders `.arena-button__root`, spelt from the component's own name, so a rule of yours written against that class is a rule about this component and nothing else. Every sheet is named the same way, which is why `css/components/arena-button.css` is the file and `arena-button` is what you write in a `stylesheet` list. **An optional value binds straight through.** Every input that carries a default resolves an absent value back to it, so a field of yours that may be unset needs no `?? '...'` at the call site: `[tone]="toast.tone"` is enough, and the default stays stated in one place, the component. It is the members' own defaults that the prompt tables carry. ## Your layout goes on a container you own **Put your spacing and sizing on an element you wrote, and let the Arena element be its child.** Arena draws no outer margin on anything, so the air between two components is always yours to place. Where you place it matters: ``, `` and `` are three of the components that take their host out of layout with `display: contents`, because their real root has to be a `