@epilot/volt-ui-experimental-griffel 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +153 -0
- package/AUTHORING.md +292 -0
- package/README.md +85 -7
- package/dist/components/alert-dialog/alert-dialog.js +18 -18
- package/dist/components/avatar/avatar.js +18 -19
- package/dist/components/avatar/avatar.js.map +1 -1
- package/dist/components/badge/badge.js +13 -13
- package/dist/components/badge/badge.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.js +6 -6
- package/dist/components/button/button.js +20 -20
- package/dist/components/button-group/button-group.js +6 -6
- package/dist/components/callout/callout.js +6 -6
- package/dist/components/card/card.js +8 -8
- package/dist/components/collapsible-sidebar/collapsible-sidebar.js +4 -4
- package/dist/components/command/command.js +15 -15
- package/dist/components/data-table/data-table-column-header.js +5 -5
- package/dist/components/data-table/data-table-error.js +13 -13
- package/dist/components/data-table/data-table-loading.js +4 -4
- package/dist/components/date-range-picker/date-range-picker.js +8 -8
- package/dist/components/date-time-picker/calendar.js +15 -15
- package/dist/components/date-time-picker/date-time-picker-panel.js +73 -67
- package/dist/components/date-time-picker/date-time-picker-panel.js.map +1 -1
- package/dist/components/date-time-picker/date-time-picker.js +53 -49
- package/dist/components/date-time-picker/date-time-picker.js.map +1 -1
- package/dist/components/date-time-picker/time-picker.js +77 -71
- package/dist/components/date-time-picker/time-picker.js.map +1 -1
- package/dist/components/dialog/dialog.js +21 -21
- package/dist/components/drawer/drawer.js +16 -16
- package/dist/components/dropdown-menu/dropdown-menu.js +13 -13
- package/dist/components/field/field-combobox.js +9 -9
- package/dist/components/field/field.js +22 -22
- package/dist/components/label/label.js +11 -11
- package/dist/components/list/list.js +13 -13
- package/dist/components/option-card/option-card.js +24 -24
- package/dist/components/pill/pill.js +21 -20
- package/dist/components/pill/pill.js.map +1 -1
- package/dist/components/popover/popover.js +4 -4
- package/dist/components/radio/radio.js +8 -8
- package/dist/components/scroll-overflow/scroll-overflow.js +6 -6
- package/dist/components/select/select.js +25 -25
- package/dist/components/skeleton/skeleton.js +17 -17
- package/dist/components/skeleton/skeleton.js.map +1 -1
- package/dist/components/table/table-pagination.js +74 -76
- package/dist/components/table/table-pagination.js.map +1 -1
- package/dist/components/tabs/tabs.js +19 -19
- package/dist/components/toast/toast.js +24 -24
- package/dist/components/toggle/toggle.js +17 -17
- package/dist/components/tooltip/tooltip.js +6 -6
- package/dist/index.d.ts +1490 -1056
- package/dist/index.js +276 -273
- package/dist/index.js.map +1 -1
- package/dist/tokens/core.generated.js +1168 -1154
- package/dist/tokens/core.generated.js.map +1 -1
- package/dist/tokens/index.d.ts +1076 -1054
- package/dist/tokens/index.js +1078 -1056
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/install.js +21 -9
- package/dist/tokens/install.js.map +1 -1
- package/dist/tokens/palette.generated.js +1323 -1293
- package/dist/tokens/palette.generated.js.map +1 -1
- package/dist/tokens.json +45 -1
- package/package.json +7 -2
- package/testing/jest-preset.cjs +64 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Agent rules — `@epilot/volt-ui-experimental-griffel`
|
|
2
|
+
|
|
3
|
+
These rules govern every file under `packages/experimental-griffel/`. They are normative: where any
|
|
4
|
+
other document in this package disagrees with this file, **this file wins** and the other one is a
|
|
5
|
+
bug worth fixing.
|
|
6
|
+
|
|
7
|
+
`packages/experimental/AGENTS.md` does **not** apply here. It governs
|
|
8
|
+
`@epilot/volt-ui-experimental-css-modules`, a different package with a different styling engine, and
|
|
9
|
+
its rules on CSS Modules, `var(--volt-*)` tokens and `bun run *:experimental` scripts are all wrong
|
|
10
|
+
for this one. The two are siblings, not parent and child.
|
|
11
|
+
|
|
12
|
+
**What the other documents are for.** `AUTHORING.md` explains the defect behind each rule — read it
|
|
13
|
+
when you want to know _why_, not _what_. `DECISIONS.md` records architecture rationale as a dated
|
|
14
|
+
log. `STATUS.md` is a snapshot of measurements and progress at a point in time; treat every number
|
|
15
|
+
in it as of its commit, never as a live fact. This file holds no counts and no measurements on
|
|
16
|
+
purpose, because those are what rot.
|
|
17
|
+
|
|
18
|
+
## Architecture, as it is now
|
|
19
|
+
|
|
20
|
+
- **Primitives are Radix**, at the same versions v1 pins, plus `sonner` for Toast and `cmdk` for
|
|
21
|
+
Command. There is no `@base-ui/react` in this package — not in the manifest, not in `src`.
|
|
22
|
+
Anything telling you to use Base UI primitives, or to prefer `data-checked` over Radix's
|
|
23
|
+
`data-state="checked"`, is describing an abandoned detour. Radix state attributes are
|
|
24
|
+
**value-based**: `&[data-state='checked']`, not `&[data-checked]`.
|
|
25
|
+
- **Styling is Griffel**, emitted as runtime atoms. No Tailwind, no `cn`, no `tailwind-merge`, no
|
|
26
|
+
shipped stylesheet. Use `mergeClasses` from `@epilot/volt-ui-runtime` — this package
|
|
27
|
+
does not import `@griffel/core` anywhere, and `gate:one-bare-specifier` enforces that.
|
|
28
|
+
- **The runtime target is React 18**, while the repo compiles against `@types/react@19`. Both suites
|
|
29
|
+
must stay green.
|
|
30
|
+
- ESM only. `react` and `react-dom` are peers; everything else is vendored into `dist`.
|
|
31
|
+
`sideEffects: false`.
|
|
32
|
+
|
|
33
|
+
## Authoring rules
|
|
34
|
+
|
|
35
|
+
1. **`&` must be the first token of every selector.** Griffel prepends its class to the whole
|
|
36
|
+
selector and substitutes `&` nowhere else, so `"[data-pressed] &"` emits
|
|
37
|
+
`.hash[data-pressed] .hash` and can never match. No warning, no error, no failing test. To style a
|
|
38
|
+
descendant from an ancestor's state, declare the rule on the _ancestor's_ slot and reach down via
|
|
39
|
+
`SLOT_ATTR`. A selector list is fine when each part is legal alone.
|
|
40
|
+
`src/ancestor-selector.test.ts` enforces this.
|
|
41
|
+
|
|
42
|
+
2. **Declaration order inside a slot encodes precedence, and v1's order does not.** Griffel emits
|
|
43
|
+
atoms in source order, so the later rule wins. v1's precedence comes from Tailwind's utility
|
|
44
|
+
layer, where `aria-invalid:` beats `disabled:` regardless of class order. Transcribing v1's order
|
|
45
|
+
inverts the outcome. Put the state that must **win** last.
|
|
46
|
+
|
|
47
|
+
3. **Match a neighbour's property shape, not just its value.** Griffel dedupes only when expanded
|
|
48
|
+
property keys match, so `paddingLeft`/`paddingRight` against a neighbour's
|
|
49
|
+
`shorthands.paddingInline` leaves both standing and the cascade picks the wrong one.
|
|
50
|
+
|
|
51
|
+
4. **No CSS shorthands** — an atom is one declaration, so a shorthand cannot be resolved against a
|
|
52
|
+
longhand. Use `shorthands.*`. Griffel only warns at runtime, so the warnings-as-errors test is
|
|
53
|
+
what catches it.
|
|
54
|
+
|
|
55
|
+
5. **Animations use the object form**, `animationName: { from: {…}, to: {…} }`. Griffel hashes
|
|
56
|
+
keyframe names from content but does **not** apply the class-name salt to them, so a hand-authored
|
|
57
|
+
global `@keyframes` collides across co-resident copies silently.
|
|
58
|
+
|
|
59
|
+
6. **`forwardRef` is required.** `ref`-as-a-plain-prop is React 19; every consumer runs React 18,
|
|
60
|
+
where a function component without `forwardRef` never receives `ref`.
|
|
61
|
+
|
|
62
|
+
7. **`asChild` is supported and should stay supported.** Components wrapping a Radix primitive
|
|
63
|
+
inherit it through `React.ComponentProps<typeof Primitive.X>` and need no work. Components that
|
|
64
|
+
render their own element — Button and BreadcrumbItem — implement it with Radix's `Slot`. v1
|
|
65
|
+
exposes it on thirteen components and consumers use it to wrap router links, so dropping it
|
|
66
|
+
breaks call sites. It was briefly removed while this package was on Base UI; that reason no
|
|
67
|
+
longer exists. Do not expose Base UI's `render` prop, which is a different thing.
|
|
68
|
+
|
|
69
|
+
8. **Colour is a value, never part of a class name.** Components with a `color` prop follow Badge:
|
|
70
|
+
atoms read inline custom properties the component sets. Never emit a per-colour rule.
|
|
71
|
+
|
|
72
|
+
9. **Call `ensurePaletteTokens()` whenever you reference any family outside
|
|
73
|
+
accent / gray / error / warning / success / info** — whether or not the component has a `color`
|
|
74
|
+
prop. Those six are in the core sheet; every other family is in the palette sheet, which is not
|
|
75
|
+
loaded by default, and referencing it without installing gives an invalid computed value and no
|
|
76
|
+
error.
|
|
77
|
+
|
|
78
|
+
10. **No module-scope DOM or stylesheet mutation.** Installing tokens, inserting styles or touching
|
|
79
|
+
`document` must happen lazily at first render, never at import time — that is what makes
|
|
80
|
+
`sideEffects: false` true and keeps an unused import free.
|
|
81
|
+
`src/components/avatar/avatar.tsx` currently calls `ensurePaletteTokens()` at module scope. **That
|
|
82
|
+
is a known defect, not a precedent.** Do not copy it.
|
|
83
|
+
|
|
84
|
+
11. **`SLOT_ATTR` for selectors, `data-slot` for diagnostics.** Emit both, and never write a selector
|
|
85
|
+
keyed on `data-slot` — those values are shared with v1, so such a rule also matches v1-rendered
|
|
86
|
+
DOM. Portalled parts each carry their own styling class and their own slot marker; a class on an
|
|
87
|
+
ancestor outside the portal does not reach them.
|
|
88
|
+
|
|
89
|
+
12. **Preserve the caller's `className` and forward DOM refs.**
|
|
90
|
+
|
|
91
|
+
## Tokens
|
|
92
|
+
|
|
93
|
+
- Use a token wherever one exists (`tokens.textSm`, `tokens.spacingElement2`, `tokens.grayA11`).
|
|
94
|
+
Where none exists, a literal is correct — say so in your report. Never
|
|
95
|
+
`calc(var(--spacing) * n)`: reintroducing Tailwind's numeric scale is what this rebuild exists to
|
|
96
|
+
avoid.
|
|
97
|
+
- **Spacing and size are different axes.** There is no dimension token family, so `width: "0.75rem"`
|
|
98
|
+
on an svg stays a literal even though `spacingElement3` shares the number.
|
|
99
|
+
- **Never edit a generated token file, and never hardcode a namespace or salt hash.** Run
|
|
100
|
+
`bun run tokens`.
|
|
101
|
+
- Core and palette carry **independent** namespaces, so a palette-only change does not rename core
|
|
102
|
+
properties. `GRIFFEL_CLASS_SALT` covers the pinned Griffel version and nothing else — token
|
|
103
|
+
isolation is the namespaces' job, not the salt's.
|
|
104
|
+
|
|
105
|
+
## Rule 0 — v1's rendered OUTPUT is the fidelity target
|
|
106
|
+
|
|
107
|
+
Not v1's source, and not a task file's summary of it. Where a task file and v1 disagree on a visual
|
|
108
|
+
value, v1 wins; where v1's source and what v1 actually renders disagree, **what it renders wins**.
|
|
109
|
+
Reproduce v1's defects when they are cosmetic, and say so. Do not reproduce one that makes a
|
|
110
|
+
component non-functional. Every intentional difference needs an explicit, reviewed VR exception.
|
|
111
|
+
|
|
112
|
+
There is one standing exception, and it is the only one: **leading**. v1's shipped sheet references
|
|
113
|
+
every `--text-*--line-height` unprefixed and so applies none of them, and v2 follows what v1
|
|
114
|
+
declares instead of what it renders. That is decided in `DECISIONS.md` D9, corrected on the v1 arm of
|
|
115
|
+
the VR harness by `V1_DECLARED_LEADING_SHIM`, and dated by `vr/v1-leading-references.test.ts`. Pair
|
|
116
|
+
every `fontSize: tokens.text*` with the matching `lineHeight: tokens.leading*` in the same slot; do
|
|
117
|
+
not read it as licence to prefer v1's source anywhere else.
|
|
118
|
+
|
|
119
|
+
## Verifying
|
|
120
|
+
|
|
121
|
+
Run these, by name — not against a remembered count:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
bun run build # tokens, tsc, vite
|
|
125
|
+
bun run test # React 19
|
|
126
|
+
bun run react18:test # React 18
|
|
127
|
+
bun run gate:vr # visual regression against v1
|
|
128
|
+
bun run gates # all architecture gates, fail-fast
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Wire your component into `src/index.ts` **before** running the gates: several build the package from
|
|
132
|
+
its entrypoint, so an unexported component is invisible to them and they will pass without having
|
|
133
|
+
seen it.
|
|
134
|
+
|
|
135
|
+
**A green gate is evidence for the assertions that gate actually makes, and nothing wider.** Every
|
|
136
|
+
gate here has a `GATE_NEGATIVE_CONTROL` mode, documented in its own header, demonstrated to trip
|
|
137
|
+
exactly the checks it should. If you add or strengthen an architectural claim, add a control that
|
|
138
|
+
breaks the property under test and show it failing. A check that cannot fail is worse than no check,
|
|
139
|
+
because it reads as coverage — this suite had two such checks for months.
|
|
140
|
+
|
|
141
|
+
## Done means
|
|
142
|
+
|
|
143
|
+
Exported from `src/index.ts`, both suites green, tsc clean, VR specimens covering the component's
|
|
144
|
+
**states** and not merely its default, and gates green. A component with no state-bearing specimens
|
|
145
|
+
is not finished.
|
|
146
|
+
|
|
147
|
+
## Working agreements
|
|
148
|
+
|
|
149
|
+
- Do not add dependencies or edit `package.json` unless the task says so explicitly.
|
|
150
|
+
- Do not commit, push, or run any git write command unless asked.
|
|
151
|
+
- Put throwaway probes in `.scratch/` (gitignored) inside this package, never in `/tmp`.
|
|
152
|
+
- Report: file paths, the vitest and tsc results, any literal used for want of a token, any behaviour
|
|
153
|
+
delta from v1, and anything you were unsure about.
|
package/AUTHORING.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Authoring volt-ui-v2 — what went wrong, and why the rules exist
|
|
2
|
+
|
|
3
|
+
`AGENTS.md` is the brief: what to do, and it is normative. This is the reasoning: what broke, how it was found,
|
|
4
|
+
and what it cost. Every item here is a defect that actually shipped into the branch, and most of them
|
|
5
|
+
passed source review, a green unit suite and a source-level audit before a rendering comparison caught
|
|
6
|
+
them.
|
|
7
|
+
|
|
8
|
+
Read this before changing a component or adding one. The single most useful thing in it is the pattern
|
|
9
|
+
at the end: **the same class of mistake recurred four times, and it was never caught by reading.**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. The engine — four Griffel constraints, all of which fail silently
|
|
14
|
+
|
|
15
|
+
### 1.1 `&` must be the first token of a selector
|
|
16
|
+
|
|
17
|
+
Writing `"[data-pressed] &"` reads as "style me when an ancestor is pressed". Griffel does not
|
|
18
|
+
substitute `&` in that position; it prepends its own class to the whole selector and emits:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
.f1y9ypwo[data-pressed] .f1y9ypwo {
|
|
22
|
+
opacity: 1;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That needs one element to carry the class AND be pressed AND contain a second element with the same
|
|
27
|
+
class. It never matches. No warning, no error, no failing test.
|
|
28
|
+
|
|
29
|
+
**Cost: fifteen dead declarations across six components**, every one a feature that silently did not
|
|
30
|
+
work — all four of Toast's type colours (every toast looked identical), OptionCard's six
|
|
31
|
+
selected-state glyphs, Label's disabled dimming, Select's chevron rotation.
|
|
32
|
+
|
|
33
|
+
Only OptionCard's was caught by the visual gate, because it was the only one with a specimen that
|
|
34
|
+
exercised the state. `src/ancestor-selector.test.ts` now forbids the form, and reads source for
|
|
35
|
+
exactly that reason.
|
|
36
|
+
|
|
37
|
+
**Instead:** declare the rule on the slot of the element that carries the state, and reach the
|
|
38
|
+
descendant by `SLOT_ATTR`:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
[`&[data-pressed] [${SLOT_ATTR}="option-card-radio-dot"]`]: { opacity: 1 }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If the ancestor belongs to another component, the rule belongs in that component — Label's disabled
|
|
45
|
+
rules live in `field.tsx`. **And there is a real capability loss here:** v1 expresses these with
|
|
46
|
+
Tailwind's `group-data-[…]:` and `peer-disabled:`, which work only because Tailwind emits _global_
|
|
47
|
+
utility classes that can be rooted anywhere. Scoped atomic CSS cannot. A Label used outside a Field,
|
|
48
|
+
next to a disabled input, will not dim.
|
|
49
|
+
|
|
50
|
+
### 1.2 Declaration order inside a slot encodes precedence
|
|
51
|
+
|
|
52
|
+
Griffel emits atoms in source order, so a later state selector beats an earlier one. **v1's precedence
|
|
53
|
+
does not come from order** — it comes from Tailwind's utility layer, where `aria-invalid:` beats
|
|
54
|
+
`disabled:` no matter where either sits in the class string.
|
|
55
|
+
|
|
56
|
+
Transcribing v1's class order therefore inverts the result. Checkbox and Radio both put
|
|
57
|
+
`&[aria-invalid='true']` before `&[data-disabled]`, so a disabled invalid control lost its error
|
|
58
|
+
border to the gray one: 17% and 44% pixel differences on a 16×16 box.
|
|
59
|
+
|
|
60
|
+
**Put the state that must win last.**
|
|
61
|
+
|
|
62
|
+
### 1.3 Dedup only happens when the expanded property keys match
|
|
63
|
+
|
|
64
|
+
OptionCard's card is a `ToggleGroupItem`. Toggle declares `shorthands.paddingInline`; the card
|
|
65
|
+
declared `paddingLeft`/`paddingRight`. Different keys, so both survived and the cascade kept Toggle's
|
|
66
|
+
value — the padding stayed 4px-per-side wrong _after the value itself had been corrected_, which is a
|
|
67
|
+
confusing thing to debug.
|
|
68
|
+
|
|
69
|
+
Match the neighbour's property shape, not just its value.
|
|
70
|
+
|
|
71
|
+
### 1.4 A typo'd slot name used to compile
|
|
72
|
+
|
|
73
|
+
`makeVoltStyles` was generic over the key union:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
makeVoltStyles<Slots extends string>(styles: Record<Slots, GriffelStyle>): () => Record<Slots, string>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Inferring a type parameter from `Record<Slots, …>` against an object literal **widens `Slots` to
|
|
80
|
+
`string`**, so the result was `Record<string, string>` and every slot name typechecked — including
|
|
81
|
+
ones that did not exist.
|
|
82
|
+
|
|
83
|
+
OptionCard read `styles.card` and `styles[layout]` with neither declared. The component rendered with
|
|
84
|
+
**no card styling at all** — v1's card properties had been copied onto the `group` slot and applied to
|
|
85
|
+
the wrong element. `tsc` clean, 322 tests green.
|
|
86
|
+
|
|
87
|
+
Now generic over the object, so `keyof S` is the real union. It found exactly three errors, all in
|
|
88
|
+
OptionCard: the hole was library-wide, the exploitation was not.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2. Shipping no reset — v2 inherits nothing v1 got for free
|
|
93
|
+
|
|
94
|
+
v1's components were never written against a reset either. They inherited whatever the _element Radix
|
|
95
|
+
happened to render_ got from the browser's UA stylesheet. Base UI renders different elements, so v2
|
|
96
|
+
inherits nothing.
|
|
97
|
+
|
|
98
|
+
**Checkbox rendered 2px wide.** Base UI's root is a `<span>`: `display: inline`, where width and
|
|
99
|
+
height **do not apply at all**. v1's Radix root is a `<button>`, which the UA sheet gives
|
|
100
|
+
`inline-block` and `border-box`. v1's own class list declares neither.
|
|
101
|
+
|
|
102
|
+
Two further subtleties from the same fix:
|
|
103
|
+
|
|
104
|
+
- `inline-block` specifically, not `inline-flex`. The box is 16×16 either way, but an inline-flex box
|
|
105
|
+
takes its baseline from its first flex item and sat the line box 2px taller.
|
|
106
|
+
- Declare `boxSizing` where the element type changed, but **not library-wide**. Where v1 also renders
|
|
107
|
+
a plain div, both arms are already `content-box` and agree — adding `border-box` there would
|
|
108
|
+
_create_ a difference. 26 slots across 17 components set a size and a border; only the ones the
|
|
109
|
+
visual gate flagged were actually wrong.
|
|
110
|
+
|
|
111
|
+
**The reset does exist in production, but do not rely on it.** `epilot360-root-config` imports
|
|
112
|
+
`tailwindcss/preflight.css` globally at the single-spa root. So `box-sizing: border-box` is there for
|
|
113
|
+
an MFE inside the portal — and absent for one bundled standalone. Explicit declarations are no-ops in
|
|
114
|
+
the first case and load-bearing in the second.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 3. The primitive layer is Radix — and the Base UI detour is why
|
|
119
|
+
|
|
120
|
+
**Historical. v2 uses Radix, at v1's exact pins.** Nothing in this section is current guidance; it is
|
|
121
|
+
here because the detour cost real time and the reasoning generalises. `src/lib/auto-portal.tsx`, the
|
|
122
|
+
`InsideMenuGroupContext` wrapper and `collectItemLabels` are all deleted — if you find a reference to
|
|
123
|
+
them, it is stale.
|
|
124
|
+
|
|
125
|
+
v2 was first built on Base UI and rolled back. The measured case against it:
|
|
126
|
+
|
|
127
|
+
- **2.1x the size** for the same primitive — tooltip 33.9k vs 16.8k gzip.
|
|
128
|
+
- Its shipped types **forced every consumer onto `@types/react` 19**: 312 errors under 18, none of them
|
|
129
|
+
in our own logic. It was the sole source of that requirement.
|
|
130
|
+
- It needed wrappers Radix does not — `<Dialog.Portal>` is mandatory, `DropdownMenuLabel` must sit
|
|
131
|
+
inside a `Menu.Group` — so **every overlay in the package rendered nothing**: nine specimens, blank
|
|
132
|
+
pages, while 321 tests and four isolation gates stayed green. The package's own tests used the
|
|
133
|
+
explicit composition Base UI documents, which is exactly why they never hit it.
|
|
134
|
+
|
|
135
|
+
Two lessons that outlive the decision:
|
|
136
|
+
|
|
137
|
+
- **Silent acceptance of an unsupported option.** `toast("…", { duration: Infinity })` was accepted,
|
|
138
|
+
did nothing, and reported nothing; all four toast specimens rendered a bare viewport.
|
|
139
|
+
- **Strictness is not uniformly one way.** v1 **throws** on `OptionCardGroup` without a `type` prop
|
|
140
|
+
where v2 tolerates it silently — so "v2 is more permissive" is not a safe generalisation, and neither
|
|
141
|
+
is its opposite.
|
|
142
|
+
|
|
143
|
+
Radix state attributes are what the code uses now: `data-state="checked" | "on" | "open" | "closed"`,
|
|
144
|
+
and enter/exit transitions key on `data-state` rather than on separate start/end hooks.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 4. Rule 0 means v1's OUTPUT, not v1's source
|
|
149
|
+
|
|
150
|
+
This is the subtlest thing in the package, and it cuts both ways.
|
|
151
|
+
|
|
152
|
+
**Four v1 production bugs were found here, all one family — the prefixer rewrites definitions and
|
|
153
|
+
misses references:**
|
|
154
|
+
|
|
155
|
+
1. `--color-level-2`/`-3` point at `--volt-level-2`/`-3`, which nothing defines.
|
|
156
|
+
2. `.volt-text-xs` resolves `line-height: var(--volt-leading, var(--text-xs--line-height))` — the
|
|
157
|
+
definition ships prefixed, the reference does not, and the unprefixed name is defined nowhere. So
|
|
158
|
+
**every text utility in v1 computes an invalid line-height and inherits instead.**
|
|
159
|
+
3. Label's `leading-none` never reaches the DOM at all: v1's `cn()` runs tailwind-merge, which treats
|
|
160
|
+
the later `text-sm` as owning line-height and drops it. Combined with (2), production v1 renders
|
|
161
|
+
labels at preflight's 1.5 rather than at 1.
|
|
162
|
+
4. ScrollOverflow's viewport declares `[&>div]:!flex` and `[&>div]:!flex-row`, and neither survives
|
|
163
|
+
the build — so v1's _horizontal_ scroller stacks its children vertically.
|
|
164
|
+
|
|
165
|
+
**The rule: reproduce the harmless ones, refuse the harmful ones.**
|
|
166
|
+
|
|
167
|
+
- ScrollOverflow does **not** reproduce bug (4). Matching it would ship a horizontal scroll container
|
|
168
|
+
that cannot scroll horizontally. That field requires a written reason and prints on every run.
|
|
169
|
+
- Nor does anything reproduce bugs (2) and (3), and that is the one **standing** exception, decided
|
|
170
|
+
in DECISIONS.md D9. Every slot that sets `fontSize: tokens.text*` also sets the matching
|
|
171
|
+
`lineHeight: tokens.leading*`, Label included — it follows the `text-sm` its class list actually
|
|
172
|
+
ships, not the `leading-none` tailwind-merge ate. Bug (2) is not cosmetic: it kills the leading at
|
|
173
|
+
every one of the seven steps, so reproducing it means shipping a type scale with no vertical
|
|
174
|
+
rhythm. It is also too wide for an `acceptedDifference` — every specimen containing text would need
|
|
175
|
+
one — so the VR harness corrects it on the v1 arm instead, once and by name
|
|
176
|
+
(`V1_DECLARED_LEADING_SHIM` in `vr/run.mjs`, pinned and dated by
|
|
177
|
+
`vr/v1-leading-references.test.ts`).
|
|
178
|
+
|
|
179
|
+
Read the exception narrowly. It is about leading and nothing else; everywhere else, output still
|
|
180
|
+
wins.
|
|
181
|
+
|
|
182
|
+
The same principle settles smaller cases without any bug involved: v1 doesn't style disabled tabs at
|
|
183
|
+
all, so v2's invented `opacity: 0.5` was removed. v1's toast declares `shadow-md` and renders a fully
|
|
184
|
+
transparent one, so v2 has no shadow. v1's OptionCard has a bare Tailwind `border` with no colour
|
|
185
|
+
utility, so its border is `currentColor` — a near-black `rgba(2, 0, 8, 0.88)` that looks like an
|
|
186
|
+
accident and is nevertheless what ships.
|
|
187
|
+
|
|
188
|
+
**When in doubt, measure the rendered result on both arms. Do not read the class list and infer.**
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 5. Retiring a dependency inherits its defaults
|
|
193
|
+
|
|
194
|
+
Toast's width (356px), corner offset (24px), zero-width border and absent shadow were all **sonner**
|
|
195
|
+
defaults, not choices in v1's source. The port replaced sonner's behaviour and kept Tailwind-flavoured
|
|
196
|
+
values — 24rem, 1rem, a real 1px border, a real shadow — so every toast was 28px too wide, 8px too
|
|
197
|
+
close to the corner, and carried an edge and a shadow v1 does not render.
|
|
198
|
+
|
|
199
|
+
Nothing but a pixel comparison would have said so. The same exposure applies to `cmdk` in Command,
|
|
200
|
+
which has no v1 export to compare against at all.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 6. The token layer
|
|
205
|
+
|
|
206
|
+
- **There was no 0.75rem spacing step, and that is why `spacingElement3` now exists.** The scale ran
|
|
207
|
+
0.25 / 0.5 / 0.5 / 1 / 1.5 / 2 rem while v1 uses Tailwind's `*-3` freely, so authors reached for the
|
|
208
|
+
nearest-_sounding_ token (`spacingGroup1`, which is **0.5rem**) and silently lost 4px per side.
|
|
209
|
+
**That exact mistake was found four times** — Toast, Command, Field, SegmentedControl — which made
|
|
210
|
+
it a scale problem, not four authoring slips. The step was added; use `tokens.spacingElement3`.
|
|
211
|
+
This paragraph used to end "use the literal `0.75rem`", which was correct only before the token
|
|
212
|
+
landed. Sizes are still literals: there is no dimension token family, so `width: "0.75rem"` on an
|
|
213
|
+
svg is not `spacingElement3` — spacing and size are different axes that share a number.
|
|
214
|
+
- **Line-height tokens are v1's own declared values, and they are applied.** This paragraph used to
|
|
215
|
+
read "deliberately unused", on the reasoning that applying them diverges from what v1 _renders_
|
|
216
|
+
(§4 bug 2). That was reversed in DECISIONS.md D9: the divergence is from v1's bug, not from v1, and
|
|
217
|
+
the harness now measures against v1-as-declared. Pair the leading with the size in the same slot,
|
|
218
|
+
`fontSize` first. Leave alone the slots that already declare a line-height — those are v1
|
|
219
|
+
`leading-*` utilities that survived tailwind-merge, so they are v1's output as well as its intent.
|
|
220
|
+
- **The namespace segment is derived from the token values.** `--volt2-<hash>-accent-9` changes
|
|
221
|
+
whenever any token changes; that is what lets two builds coexist on `:root`. **Never hardcode it.**
|
|
222
|
+
One test did and broke the moment line-height tokens were added.
|
|
223
|
+
- The accessor is 942 **named exports**, not an object literal, because a bundler cannot drop unused
|
|
224
|
+
properties of a literal. Worth 6.5 kB gzip to every partial consumer. Authoring is unchanged:
|
|
225
|
+
`import * as tokens` and keep saying `tokens.accentSolid`.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 7. Verifying — where the checks themselves were wrong
|
|
230
|
+
|
|
231
|
+
A gate that cannot fail is worse than no gate, because it launders an unverified claim. Every one of
|
|
232
|
+
these was a real false pass:
|
|
233
|
+
|
|
234
|
+
- **Run gates AFTER wiring exports.** `gates/isolation-fixture.tsx` imports the package namespace, so
|
|
235
|
+
it only inspects what `src/index.ts` exports. Running it on an unwired component is a meaningless
|
|
236
|
+
pass — that is how Select's `data-slot` violation shipped.
|
|
237
|
+
- **The visual gate reads `dist`.** The first negative control perturbed a padding by 2px and still
|
|
238
|
+
reported a clean pass, because the edit was in `src`. Staleness is now a hard error.
|
|
239
|
+
- **A blank render is not a match.** Nine overlays rendered nothing while the gate reported
|
|
240
|
+
"116/124 match", because a blank capture against a mostly-white v1 is a _small_ diff. Uniform
|
|
241
|
+
captures and thrown pages are now hard failures.
|
|
242
|
+
- **A parse error makes everything pass.** Typechecking a real consumer against v2 reported 0 errors,
|
|
243
|
+
and a positive control importing a nonexistent name also reported 0 — because the consumer's
|
|
244
|
+
TypeScript 4.9 could not parse Base UI's `.d.ts`, so the module resolved to an error type. Under TS
|
|
245
|
+
5.6 the same code reports 148.
|
|
246
|
+
- **A state nobody renders is a state nobody has checked.** That is the whole reason §1.1 needed a
|
|
247
|
+
source scan on top of the visual gate.
|
|
248
|
+
- **Identical results after a change that should have moved something is a signal.** A patch adding
|
|
249
|
+
preflight to the harness silently missed its target line; the numbers came back byte-identical and
|
|
250
|
+
only a direct `box-sizing` measurement caught it.
|
|
251
|
+
- **A test that passes in the file and fails alone has not passed.** Four overlay dismissal tests were
|
|
252
|
+
green under React 19 and red under React 18, and the component behaved _identically_ in both. Run
|
|
253
|
+
the suspect test with `-t` before you believe either colour: all four also failed under React 19 in
|
|
254
|
+
isolation. Three defects were stacked, each masking the next — see §7.1.
|
|
255
|
+
|
|
256
|
+
### 7.1 The three that hid behind each other
|
|
257
|
+
|
|
258
|
+
Worth reading as a set, because no single one of them was visible while the others stood:
|
|
259
|
+
|
|
260
|
+
1. **Testing Library's auto-cleanup never ran.** It self-registers only when the framework's
|
|
261
|
+
`afterEach` is a global, and neither vitest config sets `globals: true`. Test files had compensated
|
|
262
|
+
with `document.body.innerHTML = ""`, which detaches nodes without unmounting roots. Radix delivers
|
|
263
|
+
Escape only to the topmost `DismissableLayer`, so layers left behind by earlier tests swallowed the
|
|
264
|
+
key. The tell was `data-scroll-locked="3"` on a page containing one dialog.
|
|
265
|
+
Now registered explicitly in `src/setup.testing-library.ts`, in both configs.
|
|
266
|
+
2. **The removal assertion could not pass on its own merits.** Radix's `Presence` holds a closed node
|
|
267
|
+
until `animationend`, and jsdom runs no animations. `fireEvent.animationEnd` does not fix it:
|
|
268
|
+
jsdom implements no `AnimationEvent`, so Testing Library falls back to a plain `Event` and drops
|
|
269
|
+
`animationName` — precisely the field `Presence` gates on. Construct the event by hand
|
|
270
|
+
(`src/overlay-test-helpers.ts`). Asserting `data-state="closed"` instead would be vacuous: that
|
|
271
|
+
attribute is set before `Presence` decides anything.
|
|
272
|
+
3. **Tooltip's Escape raced its own listener.** Radix attaches it in an effect, and the `waitFor`
|
|
273
|
+
resolved on DOM presence — under React 18, before that effect ran, so the keydown landed with
|
|
274
|
+
nothing listening. `await act(async () => {})` first. React 19 flushed early enough to hide it.
|
|
275
|
+
|
|
276
|
+
The general shape: **a version-dependent test result is a claim about the test, until you have shown
|
|
277
|
+
the component behaves differently.** Here it never did.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 8. The pattern worth internalising
|
|
282
|
+
|
|
283
|
+
Four separate times, the same 4px spacing error. Fifteen dead selectors of one shape. Every overlay
|
|
284
|
+
non-functional. A component with no styling at all. None of it was found by reading code — not by
|
|
285
|
+
source review, not by unit tests, not by three source-level audit passes that each found _other_ real
|
|
286
|
+
defects.
|
|
287
|
+
|
|
288
|
+
All of it was found by rendering both versions and comparing pixels.
|
|
289
|
+
|
|
290
|
+
So: **when you change a component, run `bun run gate:vr`.** When you add one, add specimens for its
|
|
291
|
+
states, not just its default. And when a check comes back clean on something that has been wrong
|
|
292
|
+
before, find out whether the check can fail at all.
|
package/README.md
CHANGED
|
@@ -137,9 +137,14 @@ fails on the insertion-point count and `gate:class-salt-skew` fails on atom-name
|
|
|
137
137
|
|
|
138
138
|
## What is not here
|
|
139
139
|
|
|
140
|
-
`
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
`Collapsible` has no equivalent here, and neither does v1 — depend on `@radix-ui/react-collapsible`
|
|
141
|
+
directly, as both pilot MFEs do.
|
|
142
|
+
|
|
143
|
+
**Corrected in 0.2.2:** this section used to say `DataTable` was unported and "the only component
|
|
144
|
+
still missing". It has shipped since 0.2.0 — `DataTable`, `DataTableBody`, `DataTableContent`,
|
|
145
|
+
`DataTableHeader`, `DataTableLoading`, `DataTableError` and `TablePagination` are all exported. The
|
|
146
|
+
stale sentence cost real work: the Variable Builder read it and hand-built its list page on the
|
|
147
|
+
`Table` primitives.
|
|
143
148
|
|
|
144
149
|
There is no `--volt-*` compatibility layer. Consumer CSS reading v1's variable names by hand needs
|
|
145
150
|
migrating to this package's exported token bindings — 217 references across six MFEs, two thirds of
|
|
@@ -148,6 +153,73 @@ them in `entity`.
|
|
|
148
153
|
Animation is unverified. The visual harness disables it on both arms, so transitions are the one thing
|
|
149
154
|
264 matching specimens do not cover.
|
|
150
155
|
|
|
156
|
+
## Treatments: stamp the document element, not a subtree
|
|
157
|
+
|
|
158
|
+
Role tokens resolve per treatment through `[data-surface="…"]` blocks. Those are **bare attribute
|
|
159
|
+
selectors**, not `:root[data-surface=…]`, so the stamp cascades from whatever element carries it —
|
|
160
|
+
and that invites putting it on a micro-frontend root. Don't.
|
|
161
|
+
|
|
162
|
+
`Popover`, `Select`, `Tooltip`, `Dialog` and `DropdownMenu` render their content into
|
|
163
|
+
`document.body`. Griffel's atomic classes are global and travel with the element, but token
|
|
164
|
+
**values** are custom properties resolved against the element's ancestors, and portalled content is
|
|
165
|
+
no longer inside your stamped subtree. It resolves against `:root` instead. So a `glass` panel would
|
|
166
|
+
open a `plain` popover, silently, with no error and nothing in the DOM to suggest why.
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
// Right — every portal is a descendant of this.
|
|
170
|
+
document.documentElement.setAttribute("data-surface", "glass")
|
|
171
|
+
|
|
172
|
+
// Wrong — the popover this panel opens escapes the stamp.
|
|
173
|
+
mfeRoot.setAttribute("data-surface", "glass")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Two consumers have not hit this, and only because `plain` is the `:root` default: the role values in
|
|
177
|
+
the bare `:root, .light, [data-theme="light"]` block are identical to `[data-surface="plain"]`, so a
|
|
178
|
+
`plain` consumer needs no stamp at all and a portal that falls back to `:root` lands on the same
|
|
179
|
+
values. The first consumer to stamp `bordered`, `glass`, `tint` or `card` on a subtree is the first
|
|
180
|
+
to see it.
|
|
181
|
+
|
|
182
|
+
Propagating the stamp onto portalled roots would remove the constraint, and is not done here: it
|
|
183
|
+
would mean every portal component reading its trigger's nearest `[data-surface]` at mount, which is
|
|
184
|
+
five components, a new failure mode when the trigger is not yet mounted, and a behaviour change for
|
|
185
|
+
a case no consumer has reached. The **document boundary** pattern is the same mechanism used
|
|
186
|
+
deliberately — customer content stamps `data-theme="light"` plus a treatment on its own root so it
|
|
187
|
+
keeps its mode inside a dark page.
|
|
188
|
+
|
|
189
|
+
## Testing
|
|
190
|
+
|
|
191
|
+
The package is ESM-only, and Jest 26 and 27 ignore the `exports` field. Before 0.2.2 that made a
|
|
192
|
+
bare import resolve to nothing: `Cannot find module '@epilot/volt-ui-experimental-griffel'`, in a
|
|
193
|
+
suite that had been green only because no test rendered one of these components yet. 0.2.2 adds a
|
|
194
|
+
`main` field, so those resolvers find `dist/index.js` themselves.
|
|
195
|
+
|
|
196
|
+
What they still need is a transform, because the file is ESM and Jest's default
|
|
197
|
+
`transformIgnorePatterns` skips everything under `node_modules`. Take it from here:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
// jest.config.js
|
|
201
|
+
const griffel = require("@epilot/volt-ui-experimental-griffel/testing/jest-preset.cjs")
|
|
202
|
+
|
|
203
|
+
module.exports = griffel.withGriffel(
|
|
204
|
+
{
|
|
205
|
+
preset: "ts-jest",
|
|
206
|
+
transform: { "^.+\\.(ts|tsx)$": ["ts-jest"] },
|
|
207
|
+
},
|
|
208
|
+
{ alsoTransform: ["@xyflow"] }
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`withGriffel` replaces `transformIgnorePatterns` outright rather than appending to it, because Jest
|
|
213
|
+
ignores a file when **any** pattern matches — so an added pattern can never un-ignore what the
|
|
214
|
+
default `/node_modules/` already caught. Anything else you need transformed out of `node_modules`
|
|
215
|
+
goes in `alsoTransform`. Needs `babel-jest` and `@babel/preset-env` as devDependencies.
|
|
216
|
+
|
|
217
|
+
**There is no CommonJS build, deliberately.** The runtime holds module-level state: the `installed`
|
|
218
|
+
Set that dedups token sheets, and one shared renderer. A dual ESM/CJS package would hand a consumer
|
|
219
|
+
two copies of both, breaking the single-insertion-point and single-renderer properties that
|
|
220
|
+
`gate:isolation` and `gate:griffel-contract` exist to assert. One format is a correctness
|
|
221
|
+
requirement here. See `DECISIONS.md` D13.
|
|
222
|
+
|
|
151
223
|
## Verification
|
|
152
224
|
|
|
153
225
|
Every check below has a `GATE_NEGATIVE_CONTROL` mode that has been demonstrated to fail when the
|
|
@@ -164,10 +236,16 @@ fail-fast. A green gate is evidence for that gate's own assertions and nothing w
|
|
|
164
236
|
| `bun run gate:griffel-contract` | `insertionCache` still dedups, and every copy resolves the same renderer |
|
|
165
237
|
| `bunx vitest run` | 493 tests, plus 497 under React 18 |
|
|
166
238
|
|
|
167
|
-
`AGENTS.md` is the normative rule set and overrides every other document in the package.
|
|
168
|
-
states the defect behind each rule.
|
|
169
|
-
|
|
170
|
-
rules.
|
|
239
|
+
`AGENTS.md` is the normative rule set and overrides every other document in the package.
|
|
240
|
+
`AUTHORING.md` states the defect behind each rule. **Both ship in the tarball as of 0.2.2** — before
|
|
241
|
+
that this paragraph cited four documents that `files` excluded, so a consumer was told the normative
|
|
242
|
+
rules existed and had no way to read them.
|
|
243
|
+
|
|
244
|
+
The other two stay in the repository, on branch
|
|
245
|
+
[`experiment/griffel`](https://gitlab.com/e-pilot/product/frontend/volt-ui/-/tree/experiment/griffel):
|
|
246
|
+
`DECISIONS.md` at the root holds the architecture rationale, and
|
|
247
|
+
`packages/experimental-griffel/STATUS.md` is a dated snapshot of measurements — more current than
|
|
248
|
+
this file on numbers, but not authoritative on rules.
|
|
171
249
|
|
|
172
250
|
## Releasing
|
|
173
251
|
|
|
@@ -5,7 +5,7 @@ import { shorthands as c } from "../../packages/volt-runtime/dist/vendor/@griffe
|
|
|
5
5
|
import { SLOT_ATTR as n } from "../../packages/volt-runtime/dist/renderer/contract.js";
|
|
6
6
|
import { mergeClasses as d } from "../../packages/volt-runtime/dist/vendor/@griffel/core/src/mergeClasses.js";
|
|
7
7
|
import { makeVoltStyles as v } from "../../styles/make-styles.js";
|
|
8
|
-
import {
|
|
8
|
+
import { spacingElement2 as f, spacingGroup2 as p, grayA1 as N, radiusXl as z, grayA11 as F, leadingSm as W, textSm as O, grayA12 as H, fontWeightSemibold as P, leadingXl as X, textXl as j, containerXl as k, containerLg as _, containerMd as I, containerSm as L, body as B } from "../../tokens/index.js";
|
|
9
9
|
import { Button as u } from "../button/button.js";
|
|
10
10
|
const g = v({
|
|
11
11
|
overlay: {
|
|
@@ -34,7 +34,7 @@ const g = v({
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
content: {
|
|
37
|
-
backgroundColor:
|
|
37
|
+
backgroundColor: B,
|
|
38
38
|
position: "fixed",
|
|
39
39
|
top: "50%",
|
|
40
40
|
left: "50%",
|
|
@@ -43,10 +43,10 @@ const g = v({
|
|
|
43
43
|
display: "grid",
|
|
44
44
|
width: "100%",
|
|
45
45
|
maxWidth: "calc(100% - 2rem)",
|
|
46
|
-
...c.gap(
|
|
47
|
-
...c.borderRadius(
|
|
46
|
+
...c.gap(p),
|
|
47
|
+
...c.borderRadius(z),
|
|
48
48
|
...c.border("1px", "solid", N),
|
|
49
|
-
...c.padding(
|
|
49
|
+
...c.padding(p),
|
|
50
50
|
// v1 shadow-lg
|
|
51
51
|
boxShadow: "0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a",
|
|
52
52
|
outlineStyle: "none",
|
|
@@ -80,40 +80,40 @@ const g = v({
|
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
sm: {
|
|
83
|
-
maxWidth:
|
|
83
|
+
maxWidth: L
|
|
84
84
|
},
|
|
85
85
|
md: {
|
|
86
|
-
maxWidth:
|
|
86
|
+
maxWidth: I
|
|
87
87
|
},
|
|
88
88
|
lg: {
|
|
89
|
-
maxWidth:
|
|
89
|
+
maxWidth: _
|
|
90
90
|
},
|
|
91
91
|
xl: {
|
|
92
92
|
"@media (min-width: 640px)": {
|
|
93
|
-
maxWidth:
|
|
93
|
+
maxWidth: k
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
header: {
|
|
97
97
|
display: "flex",
|
|
98
98
|
flexDirection: "column",
|
|
99
|
-
...c.gap(
|
|
99
|
+
...c.gap(f),
|
|
100
100
|
textAlign: "left"
|
|
101
101
|
},
|
|
102
102
|
footer: {
|
|
103
103
|
display: "flex",
|
|
104
|
-
...c.gap(
|
|
104
|
+
...c.gap(f),
|
|
105
105
|
justifyContent: "flex-end"
|
|
106
106
|
},
|
|
107
107
|
title: {
|
|
108
|
-
fontSize:
|
|
109
|
-
lineHeight:
|
|
110
|
-
fontWeight:
|
|
111
|
-
color:
|
|
108
|
+
fontSize: j,
|
|
109
|
+
lineHeight: X,
|
|
110
|
+
fontWeight: P,
|
|
111
|
+
color: H
|
|
112
112
|
},
|
|
113
113
|
description: {
|
|
114
|
-
fontSize:
|
|
115
|
-
lineHeight:
|
|
116
|
-
color:
|
|
114
|
+
fontSize: O,
|
|
115
|
+
lineHeight: W,
|
|
116
|
+
color: F
|
|
117
117
|
}
|
|
118
118
|
}), Y = i.forwardRef(
|
|
119
119
|
function(t, a) {
|