|
|
---
|
|
|
title: Direction Contract — how a component obtains, asserts and paints its reading direction
|
|
|
type: canon
|
|
|
audience: human + agent
|
|
|
authority: canonical — the resolution chain, which attribute carries it, and which selector form may read it
|
|
|
status: current
|
|
|
enforcement: §4 only — scripts/rtl-check.ts (rules RTL-1 and RTL-2, `error`) · `npm run rtl:check`. §1–§3 and §5 have no mechanical guard; they are held by review and by the acceptance rules.
|
|
|
related:
|
|
|
resolver: src/uix/soma/direction.ts (activeDir, and why it can return undefined)
|
|
|
guard: src/uix/eidos/rtl-lint.ts · scripts/rtl-check.ts (RTL-1 · RTL-2)
|
|
|
prefs: src/arts/prefs/README.md (the app-global preference and its DOM projection)
|
|
|
architecture: docs/architecture/active-architecture.md (where prefs sits in the runtime)
|
|
|
guide: docs/guides/component-guide.md (the RTL rows of the build contract)
|
|
|
checklist: docs/guides/completion-checklist.md (the acceptance rules)
|
|
|
svg: src/uix/eidos/components/chart/README.md (§Direction — mirroring a graphic)
|
|
|
---
|
|
|
|
|
|
# Direction Contract
|
|
|
|
|
|
Reading direction is the one cross-cutting fact that every layer touches: the
|
|
|
consumer may assert it, the app derives it, soma computes with it, the DOM
|
|
|
carries it, and eidos paints from it. This chapter fixes **where each of those
|
|
|
happens** and, more importantly, **where they must not**.
|
|
|
|
|
|
There are two separate questions, and conflating them is the source of every
|
|
|
direction bug this framework has shipped:
|
|
|
|
|
|
1. **What direction does this component compute with?** — a concrete value, used
|
|
|
for arrow keys, pointer sign, placement flips, scroll maths.
|
|
|
2. **What direction does this component assert to the DOM?** — an attribute that
|
|
|
must be _absent_ when nobody asserted anything.
|
|
|
|
|
|
The answers are deliberately different types.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. The chain
|
|
|
|
|
|
```text
|
|
|
prop dir → ancestor assertion (DirectionContext) → soma.prefs.getDir() → 'ltr'
|
|
|
```
|
|
|
|
|
|
**Four links, ratified 2026-08-05.** The context link is the "implicit
|
|
|
inheritance" phase left open when the chain was first fixed: every `activeDir`
|
|
|
call **publishes** its assertion (`prop ?? inherited`) into `DirectionContext`
|
|
|
and consults the nearest ancestor's before falling back to prefs. Composition
|
|
|
carries the assertion by physics — a canon component mounted inside an asserted
|
|
|
subtree, in place or through a portal (the Portal forwards Svelte context),
|
|
|
resolves the ancestor's assertion without anyone writing `dir=` at the call
|
|
|
site.
|
|
|
|
|
|
What remains true: a component still does **not read the DOM**. The DOM `dir`
|
|
|
attribute is a _projection_ of the assertion/preference, never a source.
|
|
|
|
|
|
**The chain is split at the assertion.** `activeDir` returns the ASSERTION
|
|
|
(`prop → ctx`) and stops there — that is what the attribute stamps and what
|
|
|
crosses portals. The preference is deliberately NOT part of it: prefs reach
|
|
|
the page once, through the boot's automatic DOM projection on `<html>`, and
|
|
|
everything below inherits. Only the MATHS still needs the preference as a
|
|
|
value (it cannot read the DOM), and that tail lives in one helper:
|
|
|
|
|
|
| Link | Who runs it | Where |
|
|
|
| ---------------- | ------------------------ | -------------------------------------------------- |
|
|
|
| `prop → ctx` | `activeDir(getter)` | the **wrapper** `.svelte`, at `Provider.create(…)` |
|
|
|
| `→ prefs → 'ltr'`| `resolveDir(dir, soma)` | the **provider**, once, in `resolvedDir` |
|
|
|
|
|
|
A component with no soma provider runs the same first links through
|
|
|
`activeEidosDir` — see §7.
|
|
|
|
|
|
`activeDir` (`src/uix/soma/direction.ts`) returns
|
|
|
`Active<Direction | undefined>` — the raw assertion. The maths tail lives
|
|
|
per-provider:
|
|
|
|
|
|
```ts
|
|
|
readonly resolvedDir = $derived.by(() => resolveDir(this.opts.dir, this.soma));
|
|
|
```
|
|
|
|
|
|
**Why the chain is split.** `undefined` means _nobody asserted a direction_ —
|
|
|
neither the consumer via the prop, nor an ancestor via its own assertion, nor
|
|
|
the app via a registered `direction` preference. That is not the same fact as
|
|
|
`'ltr'`, and every `=== 'rtl'` test in the codebase erases the difference. So
|
|
|
the provider keeps both: `resolvedDir` for its own maths, and the raw
|
|
|
`opts.dir.current` for the DOM.
|
|
|
|
|
|
### Composition carries the assertion — by physics
|
|
|
|
|
|
The chain is per-component, so a component that MOUNTS another canon component
|
|
|
starts a second chain inside the first. Before the context link that second
|
|
|
chain skipped the prop, landed on prefs, and stamped a value of its own — and a
|
|
|
stamped `dir` **cuts inheritance for everything below it**: the parent asserted
|
|
|
`rtl`, the child stamped `ltr`, the subtree split. The framework carried the
|
|
|
assertion by convention (a "forward `dir=` at the call site" rule with
|
|
|
hand-written forwards) until 2026-08-05; the convention kept being missed in
|
|
|
the field, which is why it is now the resolver's job.
|
|
|
|
|
|
**In place** nothing needs writing: the child's `activeDir` consults
|
|
|
`DirectionContext` before prefs, so it resolves — and stamps — the ancestor's
|
|
|
assertion. A submenu inheriting from its parent menu is the same mechanism,
|
|
|
not a special case.
|
|
|
|
|
|
**Through a portal** the panel is rendered outside its owner's subtree, so DOM
|
|
|
inheritance cannot reach it; the Svelte context still does (the Portal forwards
|
|
|
contexts), and `soma/layers/floating` additionally composes the owner link once
|
|
|
for every overlay:
|
|
|
|
|
|
```text
|
|
|
the surface's own dir prop → the owning provider's asserted dir → omit
|
|
|
```
|
|
|
|
|
|
`FloatingProviderOpts.dir` is where the owner hands its assertion down;
|
|
|
`FloatingContent.assertedDir` is where the two meet. The consequence for anyone
|
|
|
writing an overlay:
|
|
|
|
|
|
> A `Content` wrapper passes its `dir` prop **raw** — `readableActive(() => dir)`,
|
|
|
> never `activeDir`. Running the chain there makes the value always concrete, and
|
|
|
> the owner's assertion could then never win the fallback.
|
|
|
|
|
|
### The public prop
|
|
|
|
|
|
Every component that has any direction-dependent behaviour or paint declares:
|
|
|
|
|
|
```ts
|
|
|
dir?: Direction;
|
|
|
```
|
|
|
|
|
|
`Direction` is the alias, never the union written by hand. Writing
|
|
|
`dir?: 'ltr' | 'rtl'` compiles identically and leaves the contract outside the
|
|
|
alias — widening or renaming `Direction` would not reach it, and the drift would
|
|
|
not break at type level, which is the whole point of having the alias.
|
|
|
|
|
|
> ⚠️ `OptsFromProps` **strips the `undefined`** from an optional public prop by
|
|
|
> default (`Exclude<P[K], undefined>`), which would destroy exactly the
|
|
|
> distinction §1 is built on. A component using that helper lists `dir` in the
|
|
|
> `Preserve` parameter — `OptsFromProps<P, Managed, StateKey, 'dir'>` — so
|
|
|
> `Active<Direction | undefined>` survives, and the wrapper passes the box
|
|
|
> straight through the bag: `dir: activeDir(() => dir)` is a valid
|
|
|
> `bindProps` entry (Active pass-through), keeping the call — and the context
|
|
|
> it publishes — visible in the wrapper init.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. The two attributes
|
|
|
|
|
|
| Attribute | Value | Present when | Read by |
|
|
|
| ---------- | -------------------------- | ------------------------------- | --------------------------------------- |
|
|
|
| `dir` | **raw** `opts.dir.current` | someone asserted a direction | the browser (`:dir()`, bidi, UA sheet) |
|
|
|
| `data-dir` | **resolved** `resolvedDir` | the component chose to stamp it | recipes that need an unconditional hook |
|
|
|
|
|
|
`dir` is a native HTML attribute with real platform semantics: it drives the
|
|
|
bidi algorithm, isolates its subtree, and is what `:dir()` matches against. It
|
|
|
is stamped **raw**. Stamping a defaulted `'ltr'` is what makes a component force
|
|
|
its own subtree back to LTR inside an RTL page; an omitted attribute inherits,
|
|
|
for free, with no DOM read.
|
|
|
|
|
|
`data-dir` is the component's own attribute, carrying the resolved value — so
|
|
|
where a component stamps it, it is always present, unlike `dir`. It exists for
|
|
|
the case where a recipe needs a selector that always matches: a glyph that must
|
|
|
be flipped to agree with maths the component already did. Using it is not a
|
|
|
violation of §3; it is a different attribute answering a different question.
|
|
|
|
|
|
**But it is opt-in, not routine.** The acceptance rule that visual props map to
|
|
|
`data-{prop}` does not apply here — `dir` is native, `:dir()` cannot see
|
|
|
`data-dir`, and a component that stamps `data-dir` without a recipe rule reading
|
|
|
it has only enlarged the DOM. Stamp it when a recipe needs it, and not before.
|
|
|
|
|
|
### When stamping is mandatory
|
|
|
|
|
|
> **A component that accepts `dir` MUST stamp it on the element that carries the
|
|
|
> direction-dependent paint.**
|
|
|
|
|
|
The attribute is the ONLY way the paint learns what the prop asserted. Miss it
|
|
|
and the component is split in half: the prop moves the **maths** and leaves the
|
|
|
**paint** behind. Consumer asserts `dir="rtl"` on an LTR page, the arrow keys
|
|
|
flip, and nothing mirrors.
|
|
|
|
|
|
`:dir()` is not the only reader, and reading it as the trigger for this rule is
|
|
|
too narrow. Everything below resolves against the element's own computed
|
|
|
direction:
|
|
|
|
|
|
- **`:dir()`** in the recipe;
|
|
|
- **logical properties** — `margin-inline-start`, `inset-inline-end`,
|
|
|
`text-align: start`, and `flex-direction: row`, which reverses;
|
|
|
- **SVG `text-anchor`**, which is logical (§7).
|
|
|
|
|
|
Which is to say: in practice a component with any horizontal layout at all is
|
|
|
direction-dependent, and the honest reading of the rule is that accepting `dir`
|
|
|
implies stamping it.
|
|
|
|
|
|
### The morfo declares it, the type demands the wire, the runtime stamps it
|
|
|
|
|
|
The rule above used to be a convention: one hand-written line per provider, 49
|
|
|
copies of it, and 20 of 55 components that had simply forgotten. A convention
|
|
|
repeated 49 times that a third of the catalogue breaks is not a convention — it
|
|
|
is a missing abstraction.
|
|
|
|
|
|
So the mechanism lives where contracts live. The MORFO declares that the
|
|
|
component has a direction (and which parts carry it); the requirement is
|
|
|
COMPUTED from that declaration through `soma.runtime<M>`; the runtime writes
|
|
|
the attribute:
|
|
|
|
|
|
```ts
|
|
|
// morfo — the declaration
|
|
|
direction: {}, // stamps on 'provider'
|
|
|
direction: { parts: ['trigger'] }, // a root that renders no element
|
|
|
|
|
|
// provider — the wire the type now demands
|
|
|
this.runtime = this.soma.runtime(xMorfo, {
|
|
|
dir: this.opts.dir, // the raw ASSERTION, never resolved
|
|
|
// …
|
|
|
});
|
|
|
```
|
|
|
|
|
|
A morfo that declares `direction` makes `dir` **required** — forgetting the
|
|
|
wire does not compile. A morfo that doesn't makes it **forbidden** — a
|
|
|
component with no reading direction can never gain a stray attribute, and the
|
|
|
~75 direction-less runtimes never mention the axis at all. `direction.parts`
|
|
|
is validated against the declared part tree when the morfo compiles: an
|
|
|
unknown name **throws** instead of silently stamping nothing. A provider must
|
|
|
not write `dir` into its render props — the runtime already put it there.
|
|
|
|
|
|
A secondary runtime of the SAME morfo (an item, a cell, a sentinel) meets the
|
|
|
same requirement and passes the same owner assertion; the stamp only lands on
|
|
|
`direction.parts`, which those runtimes never register.
|
|
|
|
|
|
The census guard (`src/uix/morfo/direction-census.test.ts`) closes the one
|
|
|
edge the type cannot see — the public prop and the morfo live in different
|
|
|
files — by crossing `dir?: Direction` in each component's props with the
|
|
|
morfo declaration, in both directions, with the signed exceptions listed
|
|
|
inline (`field-langs`, `waveform`, and the three pure overlays whose stamp
|
|
|
belongs to the floating layer).
|
|
|
|
|
|
**The open question is still WHICH element, not whether**, and now it is
|
|
|
answered in the declaration: `direction.parts` names the parts that carry the
|
|
|
stamp, defaulting to `['provider']`. It is the element the paint sits on:
|
|
|
|
|
|
- Ordinary components — the provider root, which is the default.
|
|
|
- A root that **renders no element** — the stamp goes to the part that IS the
|
|
|
in-place half (`dropdown-menu` → `parts: ['trigger']`).
|
|
|
- A field that paints on **both** its shell and its input —
|
|
|
`parts: ['provider', 'input']`.
|
|
|
- **Portalled surfaces** — the floating wrapper, which the floating layer
|
|
|
stamps from `assertedDir` (§1). It has to: portalled to `<body>`, it is
|
|
|
outside the component's subtree and cannot inherit from the trigger. A
|
|
|
component whose only direction-dependent paint is portalled names that part
|
|
|
(`drawer` / `float-panel` → `parts: ['content']`); one that merely composes
|
|
|
an overlay is covered provided it hands that overlay its `dir`.
|
|
|
- A component with **both** halves — a trigger in place and a menu portalled —
|
|
|
needs both.
|
|
|
- **Nothing painted from CSS at all** — geometry computed and applied by JS —
|
|
|
needs no attribute, and adding one only enlarges the DOM: the morfo simply
|
|
|
declares no `direction`.
|
|
|
|
|
|
**Ask what the paint reads and where it lives**, then name that part.
|
|
|
|
|
|
### The exception: `:dir()` translating an already-logical prop
|
|
|
|
|
|
Not every `:dir()` rule is about asserting a direction. Some components expose
|
|
|
the axis themselves, as a **logical prop** — a badge at `top-start` or
|
|
|
`top-end`, an image anchored at `start` or `end`. There the consumer has already
|
|
|
said which side they mean, in logical terms; `:dir()` is only supplying the
|
|
|
physical form CSS lacks: the sign of a `translate`, or `object-position: left`
|
|
|
where no `object-position: start` exists.
|
|
|
|
|
|
**Those components must NOT take a `dir` prop.** It would be a second knob for
|
|
|
an outcome the first knob already decides, and the two would fight rather than
|
|
|
compose — the logical anchor resolves from the ambient direction while the prop
|
|
|
claims to set it. The `:dir()` rule needs the direction that actually laid the
|
|
|
element out, which is the inherited one, by design and not by omission.
|
|
|
|
|
|
The test is whether the component has its own logical vocabulary for the axis.
|
|
|
A `Switch` has none — its thumb travels one way and direction is the only input,
|
|
|
so asserting `dir` on it is meaningful. An `Avatar.Badge` has `position`, so it
|
|
|
is not.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. Selector doctrine
|
|
|
|
|
|
| Form | Verdict |
|
|
|
| ------------------ | --------------------------------------- |
|
|
|
| `:dir(rtl)` | **the canon** |
|
|
|
| `[dir='rtl'] …` | **forbidden** |
|
|
|
| `[data-dir='rtl']` | legitimate — a different attribute (§2) |
|
|
|
|
|
|
`[dir='rtl']` fails twice. The attribute is not defaulted, so the common case is
|
|
|
an element with no `dir` at all and the selector never matches; and as a
|
|
|
descendant selector it ignores any nearer `dir` that re-declares the axis.
|
|
|
`:dir()` reads the resolved direction and has neither failure.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 4. The trap the guard exists for
|
|
|
|
|
|
**A logical anchor paired with a physical displacement.** `inset-inline-start`
|
|
|
flips with the direction; `transform: translateX(-50%)` does not. Put them in
|
|
|
the same block and the element lands a full offset off-axis in RTL — the anchor
|
|
|
moved, the transform did not.
|
|
|
|
|
|
`RTL-1` (`src/uix/eidos/rtl-lint.ts`, run by `npm run rtl:check`) is the guard.
|
|
|
It reads CSS text and flags an inline-axis translate inside a block that anchors
|
|
|
on `inset-inline*` / `margin-inline*`. The sanctioned escape is a
|
|
|
`rtl-physical: <reason>` comment when the geometry genuinely is physical.
|
|
|
|
|
|
**Know its blind spot before trusting a green run**: it sees a pairing only when
|
|
|
both halves are CSS declarations. An anchor in the recipe plus a `transform`
|
|
|
written inline by JS is the same defect and is invisible to it — as is the same
|
|
|
pairing living in a shared motion preset rather than in a recipe.
|
|
|
|
|
|
### The double flip
|
|
|
|
|
|
The opposite mistake, and the harder one to see: **a `:dir(rtl)` rule whose body
|
|
|
only re-assigns logical properties.** A logical property has _already_ mirrored
|
|
|
by the time that rule matches, so moving it from `inline-start` to `inline-end`
|
|
|
sends it back to where it started — and, since the sibling declarations were
|
|
|
left alone, it lands on the opposite edge from the thing it belongs to. An
|
|
|
indent on one side with its rail on the other is the signature.
|
|
|
|
|
|
The tell is the pair: inside a `:dir()` block, the same logical family declared
|
|
|
on **both** faces, one turned off (`0`, `auto`, `none`) and the other repainted.
|
|
|
Delete the rule and check the base declaration instead — it had already
|
|
|
mirrored.
|
|
|
|
|
|
`RTL-2` guards this shape, in the same file and the same run as RTL-1. Its
|
|
|
escape hatch is `rtl-mirror: <reason>`, a different marker from RTL-1's
|
|
|
`rtl-physical:` because it declares a different thing. Reach for it only when a
|
|
|
design breaks the mirror on purpose; the fix is almost always the deletion.
|
|
|
|
|
|
Two further physical forms the guard cannot reach, both of which must be checked
|
|
|
by eye in RTL:
|
|
|
|
|
|
- **SVG geometry and `text-anchor`** — a graphic with a _reading_ axis mirrors;
|
|
|
a _radial_ one does not. `text-anchor` is logical: leave it alone when the
|
|
|
composition mirrors, force the physical value when it does not.
|
|
|
- **Inline `transform` written by a component** — beat it with the independent
|
|
|
`rotate` / `translate` / `scale` properties, which compose with `transform`
|
|
|
instead of replacing it, rather than escalating to `!important`.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 5. Behaviour that mirrors, and behaviour that does not
|
|
|
|
|
|
Mirroring is decided by whether the interaction runs along a **reading axis**,
|
|
|
not by whether it is horizontal.
|
|
|
|
|
|
- **Mirrors** — anything the user reads or traverses in sequence: arrow-key
|
|
|
navigation (via `getDirectionalKeys`), carousel travel, a slider's value
|
|
|
ramp, a colour channel spread across the inline dimension, a heat-map's
|
|
|
columns, a calendar's months.
|
|
|
- **Does not mirror** — geometry that is intrinsically physical: radial
|
|
|
controls (a colour wheel, a dial), compass-named resize handles, a freely
|
|
|
positioned panel's coordinates, an image being panned inside a viewport.
|
|
|
|
|
|
The distinction is the same one the references draw, and it is why a colour
|
|
|
_area_ mirrors while a colour _wheel_ does not.
|
|
|
|
|
|
When a component's own geometry is physical but its handles are placed with
|
|
|
logical insets, translate the logical edge into a physical one **once**, at the
|
|
|
boundary, rather than sprinkling `=== 'rtl'` through the handlers.
|
|
|
|
|
|
### Scrolling
|
|
|
|
|
|
In RTL the CSSOM scroll model runs **negative**: `scrollLeft` rests at `0` at
|
|
|
the start of the content and decreases to `-(scrollWidth - clientWidth)`.
|
|
|
Distance along the reading axis and distance from the physical left edge are
|
|
|
therefore two different quantities, and a component that needs both must name
|
|
|
both.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 6. The app-global half
|
|
|
|
|
|
`prefs.direction` is the single source of the _app's_ effective direction: it
|
|
|
derives from `prefs.language` unless an explicit intent — or the environment
|
|
|
seed — overrides it. `ActivePrefsDomProjection` writes that value to
|
|
|
`<html dir>` — together with `<html lang>`, which travels with it because the
|
|
|
browser reads both from the DOM for font selection, hyphenation and
|
|
|
screen-reader announcement.
|
|
|
|
|
|
**The projection is automatic in standalone boot** (ratified 2026-08-05):
|
|
|
components stamp only ASSERTED directions, so the page must reflect the
|
|
|
ambient preference or nothing does. `projectPrefs: false` opts out when the
|
|
|
app owns `<html>` (i18n by routing, its own projection); in `attachActiveUix`
|
|
|
it is opt-IN for the same reason. And the boot SEEDS the environment from a
|
|
|
hand-set `<html dir>` (`readPrefsEnvironmentFromDom`) so the projection adopts
|
|
|
the page's own assertion instead of rewriting it with the language-derived
|
|
|
value — precedence `intent > environment seed > derive(language) > default`.
|
|
|
|
|
|
That projection is the reason the common case needs no per-component
|
|
|
assertion at all: the page declares its direction once at the root, every
|
|
|
component inherits it, and `:dir()` resolves correctly with no attribute in
|
|
|
between. The per-component prop exists for the other case — asserting a
|
|
|
direction for **one subtree** against the page's.
|
|
|
|
|
|
### Server rendering owns the first paint
|
|
|
|
|
|
The projection runs in the client. The `<html dir>` of the FIRST render
|
|
|
therefore belongs to `app.html` or the server (the react-aria model): an app
|
|
|
whose default direction is RTL sets `<html dir="rtl" lang="ar">` at the
|
|
|
document template — the seed adopts it, the projection keeps it, and there is
|
|
|
no LTR flash. Components ship no `dir` in SSR output unless the consumer
|
|
|
asserted one, which is the same rule they follow in the client.
|
|
|
|
|
|
Detail of the preference and its projection: [`src/arts/prefs/README.md`](../../src/arts/prefs/README.md).
|
|
|
|
|
|
---
|
|
|
|
|
|
## 7. Components with no soma provider
|
|
|
|
|
|
A few components are eidos-only: no provider, no morfo, no `Soma`. The chain
|
|
|
still applies to them — only the entry points differ, because the maths tail
|
|
|
is handed a `Soma` in one layer and an `ActivePrefs` in the other:
|
|
|
|
|
|
| Layer | Assertion | Maths tail |
|
|
|
| ----- | ------------------------ | ----------------------------------- |
|
|
|
| soma | `activeDir(getter)` | `resolveDir(dir, soma)` |
|
|
|
| eidos | `activeEidosDir(getter)` | `resolveEidosDir(dir, eidos.prefs)` |
|
|
|
|
|
|
Both assertion entry points publish into the SAME `DirectionContext` — shared
|
|
|
on purpose, so an eidos-only chart inside an asserted soma subtree (or the
|
|
|
reverse) resolves the same fact. Both return `Direction | undefined`; the raw
|
|
|
stamp is the consumer's, exactly as in §1 and §2. The adapter in the maths
|
|
|
tail is the whole difference: `ActiveEidos.prefs` is the raw arts service,
|
|
|
which has no `getDir()` — that method lives on the soma-facing view.
|
|
|
|
|
|
The chart family is the worked example. It resolved by reading
|
|
|
`getComputedStyle(node).direction` off its own element until the entry point
|
|
|
existed, because the chain was genuinely not runnable from eidos.
|
|
|
|
|
|
> ⚠️ **`dir` on an `<svg>` does nothing.** The attribute is mapped to the
|
|
|
> `direction` property by the HTML UA stylesheet, which does not reach SVG
|
|
|
> elements — measured in Chrome, an `<svg dir="ltr">` under a `dir="rtl"`
|
|
|
> ancestor still computes `rtl`. Stamp the chart's HTML wrapper (`<figure>`,
|
|
|
> `<div data-chart-frame>`) instead. This matters more than it looks: SVG
|
|
|
> `text-anchor` is LOGICAL, so it resolves against the inherited direction. Get
|
|
|
> the stamp wrong and the maths mirrors while the labels do not, which is §2's
|
|
|
> split-in-half failure wearing a different hat.
|
|
|
|
|
|
The mirroring rules a graphic must obey are worked out per chart in
|
|
|
[`src/uix/eidos/components/chart/README.md`](../../src/uix/eidos/components/chart/README.md) §Direction.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 8. Checklist
|
|
|
|
|
|
When you touch a component's direction behaviour:
|
|
|
|
|
|
1. Is `dir?: Direction` declared, using the alias?
|
|
|
2. Does the **wrapper** run `activeDir(() => dir, soma)`? (A portalled
|
|
|
`Content` is the exception — it passes the prop raw; §1.)
|
|
|
3. Does the MORFO declare `direction` (with `parts` when the paint is not on
|
|
|
the provider root), and does the provider wire `dir: this.opts.dir` — the
|
|
|
raw assertion? (Either half missing does not compile; the census guard
|
|
|
crosses prop and morfo.) Does the provider default once, in `resolvedDir =
|
|
|
resolveDir(this.opts.dir, this.soma)`, and never elsewhere? In-place
|
|
|
children need nothing — `DirectionContext` carries the assertion — but a
|
|
|
PROVIDER a component creates directly (every picker's
|
|
|
`PopoverProvider.create`) still receives its `dir` opt: provider creation is
|
|
|
not a component boundary, so no context is published in between.
|
|
|
4. Does the recipe branch with `:dir()`? Then is the raw `dir` stamped?
|
|
|
5. Does any block pair a logical anchor with a physical displacement?
|
|
|
(`npm run rtl:check`)
|
|
|
6. Are the SVG, the inline transforms and the animation presets checked **by
|
|
|
eye, in RTL** — the three places the guard cannot see?
|