# `MishkaCmsCore.Runtime.PageBuilder.Tree.Styles`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/tree/styles.ex#L5)

Turns the inspector's recorded overrides into Tailwind utilities, and reads them back out again.

## Why classes and not a stylesheet

ADR-8. The runtime Tailwind compiler only ever sees classes that appear in **server-rendered
HTML**, so a rule invented at runtime and injected as CSS would never be generated. Writing the
override as `max-[900px]:py-[14px]` on the element puts it exactly where the compiler looks. It
also keeps the house rule intact — no `<style>` blocks, no static inline styles — and means an
override is legible to anyone reading the template afterwards.

## Round trip

`to_classes/1` appends generated utilities **after** whatever the author wrote, in a stable order.
`from_classes/1` recognises the same shapes on the way back in and lifts them into `meta.styles`,
leaving the author's own classes untouched. Parsing a template the editor saved therefore returns
the same override map the inspector recorded — which is what makes the responsive panel show real
state rather than a guess.

A hand-written `hover:bg-[#111]` is absorbed too. That is deliberate: it means an override someone
typed by hand becomes editable in the panel instead of being invisible to it, and it re-serializes
byte-identically.

A class is read only when writing its override back gives that same class, and only when it is the
one class in its bucket that override would replace. Two of them — `text-[#fff] text-[#000]`, or
`p-[20px]` beside an authored `px-8` — stay as the author wrote them: one value in the panel could
only give one of them back.

## Buckets

`:base` has no prefix. `:tablet` and `:mobile` are max-width queries at the prototype's
breakpoints (900 and 560). `:hover`, `:focus` and `:active` are the state variants.

# `part`

```elixir
@type part() :: {:classes, [String.t()]} | {:logic, String.t()}
```

One element of a `class={[…]}` expression: the classes a literal string holds, or the source of
something the author works out at render time.

# `add_expr_classes`

```elixir
@spec add_expr_classes(String.t(), [String.t()]) :: String.t() | nil
```

A `class={…}` expression's source with `classes` added to it as literals, or `nil` for one nothing
can be added to.

A list is extended; anything else is the author's own expression, which is not ours to take apart,
so it becomes the first element of a list the classes join.

## Why the splice is read back before it is kept

AN EXPRESSION IS NOT ALWAYS SOMETHING A COMMA CAN FOLLOW. `class={if @x, do: "a", else: "b"}` ends
in a keyword list written without brackets, and `[if @x, do: "a", else: "b", "pb-0167f46c"]` is a
syntax error: a page that no longer compiles and can no longer be saved, wearing a name that reads
back as no name at all — so the element cannot be found by it, cannot give it up, and is handed a
second one on every visit to its Custom CSS box.

Building the source and parsing it again is the only thing that actually knows. Bracketing it is
tried first because that is the shape every list class already has; parenthesising the author's
expression is what rescues the keyword-list tail; and an expression neither reaches is one this
cannot write, which is `nil` rather than a guess.

# `axes`

```elixir
@spec axes(atom()) :: {atom(), atom()}
```

The width and the state a bucket stands for — the inverse of `bucket/2`.

# `bucket`

```elixir
@spec bucket(atom(), atom()) :: atom()
```

The bucket a width and a state name together.

`{:mobile, :hover}` is `:mobile_hover`; a state at the widest breakpoint keeps its own name,
because `hover:` with no query already applies at every width.

# `bucket_atom`

```elixir
@spec bucket_atom(term()) :: {:ok, atom()} | :error
```

The bucket a name stands for, `{:ok, bucket}`, or `:error` for a name no bucket has. For a name
that came from the browser, which must never be made into an atom.

# `buckets`

```elixir
@spec buckets() :: [atom()]
```

The style buckets, in the order their classes are emitted.

# `controls_for`

```elixir
@spec controls_for(String.t()) :: [String.t()]
```

The style controls a hand-written CSS property overrides — `[]` when the panel has no say in it.

`"padding-block"` answers `["py", "pt", "pb"]`: the control that owns the property outright, plus
the longhands a shorthand swallows. Used by the inspector to mark a control whose value the
element's own CSS has taken over, and by `from_declaration/2` in reverse.

# `declaration`

```elixir
@spec declaration(String.t(), String.t()) :: {String.t(), String.t()}
```

One recorded override as the CSS declaration it means, for anything that shows an author their own
styling. `{"py", "16px"}` reads `padding-block: 16px`, and `{"align", "items-center"}` reads
`align-items: center`.

A property this module names the same way CSS does, and a free-form value, pass straight through.

# `dependents`

```elixir
@spec dependents(String.t(), String.t() | nil) :: [String.t()]
```

The overrides that stop meaning anything once `prop` is set to `value`, and should go with it.

Lives here rather than in a panel because the vocabulary is what knows which properties depend on
which — so the toolbar, the Inspector and anything added later all drop the same set, in the same
single undoable step.

# `from_classes`

Splits a class attribute into the author's own classes and the override map the editor recorded.

Returns `{authored_classes, styles}`. Anything that does not look like a generated utility stays
in `authored_classes` untouched and in its original order.

# `from_declaration`

```elixir
@spec from_declaration(String.t(), String.t()) ::
  {:ok, {String.t(), String.t()}} | :error
```

One hand-written declaration as the override it would become, or `:error` when no control can hold
it — which is what decides whether **Move to controls** is offered beside it.

Exact only: `padding: 20px` becomes the Padding override and never the four longhands it also
overrides, because moving it into `pt` alone would change what the page looks like.

# `gap_presets`

```elixir
@spec gap_presets() :: [{String.t(), String.t()}]
```

The one-click spacing values offered beside the gap field.

# `labels`

```elixir
@spec labels(String.t()) :: [{String.t(), String.t(), String.t(), String.t()}]
```

Each value of a property whose options are explained by their own LABEL rather than by a picture.

`{value, title, tailwind, css}`. Returns `[]` for anything pictured or free-form, so a panel can
ask both and draw whichever answers.

# `lengths`

```elixir
@spec lengths() :: [String.t()]
```

The properties whose value is a CSS LENGTH, and which therefore read a bare number as pixels.

Public because the panel labels them: a box that takes `50%` and `2rem` and says only "unset" is a
box people conclude is pixels-only, which is what happened.

# `list_expr`

```elixir
@spec list_expr([part()]) :: String.t() | nil
```

The `class={[…]}` expression source `parts` spell, or `nil` when nothing of them is left.

Nothing left means the attribute goes, the same as an emptied string class: a list holding only
the name an element has just given up is not a class.

# `list_parts`

```elixir
@spec list_parts(String.t()) :: [part()] | :not_a_list
```

A `class={[…]}` expression taken apart in order: `{:classes, [class]}` for each literal string
element and `{:logic, source}` for everything the author works out at render time.

Returns `:not_a_list` for an expression this cannot take apart — the same expressions
`split_list_expr/2` refuses.

## Why the classes inside a list have to be readable and writable

A CLASS IN A LIST IS A CLASS THE ELEMENT WEARS. The minted `pb-xxxxxxxx` that names an element to
its own CSS rule is spliced in here when the class is an expression
(`MishkaCmsCore.Runtime.PageBuilder.Tree.ensure_scope/2`) — flattening the list to a string to add
it would write the literal text `@variant` into the page. So every reader of a scope and every
writer of one comes through this and `list_expr/1`: a caller that instead rebuilt the attribute
from the classes it could see destroyed the author's own logic, and one that could not see them
minted a second name for an element that already had one.

# `merge_class`

```elixir
@spec merge_class(String.t() | nil, map(), keyword()) :: String.t() | nil
```

The class attribute a node should serialize with: the author's classes followed by the compiled
overrides. Returns `nil` when there is nothing to write, so an override-free node with no class
keeps having no class attribute.

Every authored class is kept unless an override in `styles` replaces it — one that would read as
an override but has none is the author's, and stays.

# `options`

```elixir
@spec options(String.t()) :: [String.t()]
```

The fixed set of values a property accepts, or `[]` when it takes a free-form value.

This is what tells the inspector to render a select instead of a text field.

# `pictures`

```elixir
@spec pictures(String.t()) :: [
  {String.t(), String.t(), String.t(), String.t(), pos_integer()}
]
```

Each value of an enumerated property with the words and the picture that explain it.

`{value, title, tailwind, css, bars}` — `tailwind` for the admin panel, `css` for the toolbar
inside the canvas iframe, which is served the author's stylesheet and has no admin classes.
Returns `[]` for a free-form property.

# `properties`

```elixir
@spec properties() :: [String.t()]
```

The CSS-ish property names the panel can express as a utility.

# `split_list_expr`

```elixir
@spec split_list_expr(String.t(), keyword()) :: {[part()], map()} | :not_a_list
```

A `class={[…]}` expression split into the part that is ours and the part that is the author's.

Returns `{parts, styles}` where `parts` is `list_parts/1`'s reading of the list with the
recognised utilities taken out of its classes, or `:not_a_list` for an expression this cannot
safely take apart — `class={@variant}`, a function call, anything that is not a literal list.

## Why a list is worth taking apart and `@variant` is not

A list is the shape the HEEx guidance asks for and the shape every kit component uses, so it is
where an override actually lands in practice. Its STRING elements are plain classes and can be
read exactly as a string class is; everything else — `@flag && "x"`, a call, a variable — is the
author's logic and passes through untouched, in place.

Without this the round trip was one-way: `Serializer.computed_class/4` appended the override and
the next parse could not see it, so a second edit nested another list and kept the first value —
`[[["p-2", …], "w-[1000px]"], "w-[900px]"]` — two classes of equal specificity where the stale one
can win.

# `to_classes`

```elixir
@spec to_classes(map(), keyword()) :: [String.t()]
```

Compiles an override map into a list of Tailwind classes, in bucket order.

Returns `[]` for an empty map, so a node with no overrides gains no classes and its `class`
attribute is left exactly as the author wrote it.

# `value_title`

```elixir
@spec value_title(String.t(), String.t()) :: String.t()
```

The plain-words name of one enumerated value, or the value itself when nothing describes it.

# `with_important_variants`

```elixir
@spec with_important_variants([String.t()], String.t()) :: [String.t()]
```

`classes` with each one that sets `prop` under a variant made important: `hover:text-white`
becomes `hover:text-white!` when `prop` is `"color"`.

A component's override is written important, so it beats the kit's own classes, and it beats the
author's variants beside it just the same: a link given a colour stopped changing on hover. With
both important, Tailwind's order decides again, and it puts the variant last.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
