flet-box 1.0.9 → 1.1.0

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.
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: fletbox-memory
3
+ description: The project journal that lets any AI pick up where the last one stopped - read .fletbox/memory/MEMORY.md before working, write an entry the moment something is learned, keep a running status note so a screen, a fix or a half-finished flow survives a change of model. Use at the start and the end of every session, before a risky change, and whenever the user says "continue" or "what is the state".
4
+ ---
5
+
6
+ # Memory — the handover between sessions
7
+
8
+ An app is built across many sessions and, more often than not, by different
9
+ models. None of them remembers the last one. The only thing that carries over is
10
+ what is written down, so writing it is part of the work, not a courtesy at the end.
11
+
12
+ A session that learned something and did not write it is a session that will have
13
+ to be repeated.
14
+
15
+ ## MEM1 Read before writing code
16
+
17
+ At the start of a session, in this order:
18
+
19
+ 1. `AGENTS.md` — the contract.
20
+ 2. `.fletbox/memory/MEMORY.md` — one line per entry, with what it is about.
21
+ 3. The entries the task touches: the current status note, and any entry naming the
22
+ screen, widget or flow you are about to change.
23
+
24
+ If the index points at a file that is not there, that is itself a finding: fix the
25
+ index before you trust it, and say so.
26
+
27
+ ## MEM2 One entry per fact, never one entry per day
28
+
29
+ `.fletbox/memory/notes/<slug>.md`, kebab-case, named after the **fact**, not the
30
+ session: `checkout-form.md`, `listview-itemsize.md`, `dark-contrast.md`. Ten files
31
+ about real things beat one `notes/2026-10-24.md` nobody can search.
32
+
33
+ Every entry has the same shape, and the last two lines are what make it usable a
34
+ month from now:
35
+
36
+ ```markdown
37
+ ---
38
+ name: listview-itemsize
39
+ description: ListView virtualises — itemSize must match the row it renders
40
+ metadata:
41
+ type: project
42
+ ---
43
+
44
+ A `ListView` row taller than its `itemSize` is clipped, and the scroll math drifts
45
+ with it. Set `itemSize` to the real row height, or leave the list unvirtualised by
46
+ passing `children` instead of `data`.
47
+
48
+ **Why:** the row looked fine at the top of the list; only after 30 items did the
49
+ bottom of each one disappear. Nothing in `npm run ui` can see it.
50
+
51
+ **How to apply:** whenever a `ListView({ data, renderItem })` is added or a row's
52
+ content changes, re-measure the row and update `itemSize`.
53
+ ```
54
+
55
+ `type` is `project` (what this app is and where it is going), `decision` (a choice
56
+ with a reason behind it), `finding` (something the framework or a screen does that
57
+ surprised you), or `task` (open work someone has to finish).
58
+
59
+ Then add the pointer to `MEMORY.md` — one line, under 150 characters, with a hook:
60
+
61
+ ```markdown
62
+ - [ListView itemSize](notes/listview-itemsize.md) — a row taller than itemSize is clipped; re-measure after any row change
63
+ ```
64
+
65
+ ## MEM3 Keep one status note, and update it as you work
66
+
67
+ `notes/current-status.md` is the handover document: the next model reads it and
68
+ knows where it is standing. Keep four sections, short, and rewrite them instead of
69
+ appending to them.
70
+
71
+ ```markdown
72
+ # Status
73
+
74
+ **Doing:** the checkout form — step 2 (payment) is built, step 3 is stubbed.
75
+ **Done and verified:** `npm run check` clean, `npm run ui` clean, 375px and 1440px
76
+ looked at in both themes.
77
+ **Not done / half-done:** the "Apply coupon" action has no loading state; the empty
78
+ cart is a blank panel.
79
+ **Next:** add the two missing states (rule 12), then wire `onEndReached` on the
80
+ product list.
81
+ **Blocked:** the coupon endpoint is not decided — ask, do not guess.
82
+ **Last touched:** src/screens/Checkout.js, 2026-10-24.
83
+ ```
84
+
85
+ Honesty here is the whole value of the file. A note that says "done" when the empty
86
+ state is missing is worse than no note: the next session builds on top of it.
87
+
88
+ ## MEM4 What belongs in memory, and what does not
89
+
90
+ Write it when it passes one of these tests:
91
+
92
+ - **You would get it wrong again.** A prop that silently does nothing, a widget that
93
+ ignores `elevation`, a default that surprises — re-deriving these costs a session.
94
+ - **It explains a rule you broke.** The one-sentence justification for a kept
95
+ `npm run ui` finding belongs next to the finding.
96
+ - **It is a decision with a reason.** "The list is unvirtualised because rows have
97
+ variable height" tells the next session not to "optimise" it back.
98
+ - **It is user preference.** Tone, language, what they call the feature, what they
99
+ refused. This is the most valuable kind and the first thing a new model loses.
100
+
101
+ Do not write:
102
+
103
+ - Anything the code or `.fletbox/api/api.json` already says. Memory that restates
104
+ the API goes stale the next release; the generated files do not.
105
+ - A log of what you edited. `git log` knows; a journal of diffs is noise that pushes
106
+ the useful entries out of view.
107
+ - The scratch plan for a task you are about to finish. Delete it when the work lands,
108
+ or keep the decision and drop the plan.
109
+
110
+ ## MEM5 Correct the record instead of arguing with it
111
+
112
+ Memory is a claim about a point in time, not a law. Before acting on an entry that
113
+ names a file, a prop or a behavior, check that it is still true:
114
+
115
+ - Still accurate → use it.
116
+ - Contradicted by the code, by `npm run check` or by what the screen does → **the
117
+ code wins.** Fix the entry, or delete it, in the same change that fixes the screen.
118
+ - About work that is now finished → remove the `task` entry and fold whatever was
119
+ learned into a `finding` or `decision` entry.
120
+
121
+ Never leave a wrong entry because deleting it feels like losing history. A stale
122
+ entry is the mechanism by which one model's mistake becomes the next model's
123
+ "existing pattern".
124
+
125
+ ## MEM6 Write before the session ends, not after
126
+
127
+ Update the status note at each natural boundary — a screen that passes both gates, a
128
+ fix confirmed in the browser, a decision taken, a blocker found. Do not plan to write
129
+ it "at the end": the end of a session is the one moment that reliably does not
130
+ happen.
131
+
132
+ One line in `MEMORY.md`, one file in `notes/`, and the status note rewritten is the
133
+ whole cost. If the change is big enough to commit, the memory change goes in the same
134
+ commit — the code and the reason for it should not be able to drift apart.
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: fletbox-motion
3
+ description: When a FletBox screen may move and with which API - AnimatedBox, AnimatedText, ParallaxBox, animate/fadeIn/pulse, the frequency gate, durations that match the interaction, the two easing vocabularies the framework actually has, and what must never animate. Use when adding any transition, entrance, loading pulse, hover lift, parallax or hero motion.
4
+ ---
5
+
6
+ # Motion — the only animation contract this app has
7
+
8
+ Motion is a sentence, not a coat of paint. One screen says one thing with it:
9
+ "this appeared", "this is loading", "this is where you are". Every other movement
10
+ is noise that costs the frame budget and the user's attention.
11
+
12
+ ## M0 The frequency gate, first
13
+
14
+ Ask how often the user sees this interaction. That number decides everything else:
15
+
16
+ | Seen | Verdict |
17
+ | --- | --- |
18
+ | 100+ times a day (a button, a form field, a list row, a tab) | **No animation.** Feedback is a color or a state, not a movement. |
19
+ | 10–100 times (a modal, a drawer, a filter applied) | 120–250ms, one property, no choreography. |
20
+ | First time only (an onboarding screen, a hero, a landing page) | This is where the budget lives: entrance, stagger, one parallax. |
21
+
22
+ A screen the user works in all day gets *less* motion than a page they visit once.
23
+ If you cannot say which row of this table the interaction is in, it gets no
24
+ animation.
25
+
26
+ ## M1 The API is four things
27
+
28
+ - **`AnimatedBox`** — declarative keyframes on one child:
29
+ `AnimatedBox({ child, animations: [{ effect, from, to, duration, reverse, loop, delay }], timing, delay, fillMode })`.
30
+ - **`AnimatedText`** — the same steps across the characters of a text, with
31
+ `delayBetween` (a stagger in ms) and `sameTime: true` to fire it at once.
32
+ - **`ParallaxBox`** — offset driven by input: `type: 'scroll' | 'mouse' | 'hover'`,
33
+ `speed`, `direction: 'vertical' | 'horizontal' | 'both'`, `maxOffset`, `reverse`,
34
+ `disabled`, `onParallaxMove`.
35
+ - **The imperative helpers** — `animate(widget, property, from, to, duration, easing)`
36
+ which returns a cancel function, `animateAsync` which returns a Promise,
37
+ `fadeIn` / `fadeOut` / `pulse` (+ the `…Async` forms), `applyPulse`,
38
+ `stopAnimation(widget, property)`.
39
+
40
+ Anything else — a hand-written `transition: all`, a CSS library, a `requestAnimationFrame`
41
+ loop in a widget — is a second animation system, and this app already has one.
42
+
43
+ **Effects `AnimatedBox` knows:** `scale`, `rotate`, `rotateX`, `rotateY`,
44
+ `translateX`, `translateY`, `opacity`, `color`, `backgroundColor`, `borderColor`,
45
+ `borderWidth`, `borderRadius`, `width`, `height`, `boxShadow`, `background`,
46
+ `textGradient`. Anything outside that list does nothing, silently.
47
+
48
+ ## M2 Duration follows the distance, and the interaction
49
+
50
+ Milliseconds, in the ranges that read as "instant" instead of "smooth" — smooth is
51
+ what slow feels like after the third repetition:
52
+
53
+ | Interaction | duration | What moves |
54
+ | --- | --- | --- |
55
+ | Press feedback | 100–160 | `scale` 1 → 0.97 |
56
+ | Hover / focus state | 120–180 | `opacity`, `borderColor` |
57
+ | Tooltip, chip, badge | 125–200 | `opacity` |
58
+ | Menu, dropdown, sheet | 150–250 | `opacity` + `translateY` 8 → 0 |
59
+ | Modal, drawer | 200–400 | `translateX`/`translateY`, never scale |
60
+ | Screen entrance, hero | 300–500 | `opacity` + `translateY` 16 → 0 |
61
+ | Loading | loop | `Skeleton({ animation: 'wave' })` |
62
+
63
+ Nothing in the UI exceeds 500ms. A 300ms number on every element is not a design
64
+ decision — it is the default.
65
+
66
+ ```js
67
+ // A sheet entering: 220ms, distance 24, nothing else.
68
+ AnimatedBox({
69
+ child: panel,
70
+ animations: [
71
+ { effect: 'opacity', from: 0, to: 1, duration: 220 },
72
+ { effect: 'translateY', from: 24, to: 0, duration: 220 },
73
+ ],
74
+ });
75
+ ```
76
+
77
+ ## M3 Two easing vocabularies — do not mix them
78
+
79
+ This is a real trap, and it fails silently:
80
+
81
+ - **`animate(..., easing)`** takes a *framework name*: `'linear' | 'easeIn' | 'easeOut' |
82
+ 'easeInOut'`. An unrecognized string falls back to `linear`.
83
+ - **`AnimatedBox({ timing })` and `ParallaxBox({ easing })`** are passed straight to
84
+ CSS: `'ease' | 'ease-out' | 'ease-in' | 'linear' | 'cubic-bezier(…)'`. `easeIn` in
85
+ a `timing` string is not a curve CSS knows, and the step runs linear.
86
+
87
+ Rules that hold for both:
88
+
89
+ - Entering and exiting UI use an **out** curve (`easeOut` / `ease-out`) — it starts
90
+ fast, which reads as responsive.
91
+ - **Never** `easeIn` on an interface element: it starts slow, so the first 100ms of
92
+ a press feels broken. Save it for something leaving the screen at speed.
93
+ - A loop that has to feel like a machine uses `linear`.
94
+ - Never `transition: all`, and never animate `width`, `height`, `top`, `left` or
95
+ `boxShadow` for something the user presses repeatedly: they reflow or repaint the
96
+ page. `AnimatedBox` *can* animate them, which is exactly why this rule needs to be
97
+ written down. Transform and opacity are the two properties that cost nothing:
98
+ `scale`, `translateX/Y`, `rotate`, `opacity`.
99
+ - Nothing enters from `scale: 0`, and nothing from full scale on a big element: use
100
+ **0.9–0.97**. From zero, the element is a dot that pops.
101
+
102
+ ## M4 Press feedback, done right
103
+
104
+ A press is 100+ times a day (M0): the answer is a state change, not a show.
105
+
106
+ ```js
107
+ // The whole of it: 140ms, 3% of scale, back on release.
108
+ onPress: (widget) => animate(widget, 'scale', 1, 0.97, 140, 'easeOut');
109
+ ```
110
+
111
+ `Button` already changes color on press. Adding a lift, a glow and a scale on top
112
+ of that is three simultaneous answers to one question. Pick one — and if the color
113
+ change is enough, which it usually is, ship it and move on.
114
+
115
+ ## M5 Entrance and stagger
116
+
117
+ Motion on first paint is the one place a longer budget is fair, with two limits:
118
+
119
+ - Stagger **30–80ms** between siblings, and cap the total. `AnimatedText` gives this
120
+ for free with `delayBetween: 40`; a hand-built list needs `delay` per step, and a
121
+ list of 20 rows at 60ms each is a 1.2s wait before the content is readable.
122
+ Animate the first 5–8 items and let the rest appear.
123
+ - One direction per screen. Elements arriving from the left *and* the bottom *and*
124
+ scaled is three ideas. A short `translateY` (8–24) plus `opacity` reads as one.
125
+
126
+ ## M6 Loops, ambient motion and `ParallaxBox`
127
+
128
+ - A loop is a status. `loop: true`, `pulse`, `applyPulse` and `MatrixRain` say
129
+ "waiting" or "this is the one". If it never stops and means nothing, turn it off.
130
+ - Loading uses `Skeleton({ animation: 'wave', count, gap, height })` in the shape of
131
+ the content that will replace it — not a spinner in the middle of a blank page.
132
+ - `ParallaxBox` belongs to a hero or a single decorative layer, never inside a
133
+ working screen, and `type: 'mouse'` never on a touch-first app. Keep `maxOffset`
134
+ small (8–24). A background that moves 120px is not depth, it is a screensaver.
135
+ - Ambient motion gets its own budget: one per screen, at most.
136
+
137
+ ## M7 What must never move
138
+
139
+ - Text the user is reading at length; body copy that animates is copy they re-read.
140
+ - Anything under a pointer or a finger while they aim at it — a control that shifts
141
+ while being targeted is a control you cannot hit (this is why press feedback scales
142
+ the element instead of moving it).
143
+ - A field while typing, a list while scrolling, a number while it is being compared.
144
+ - Two things at once in the same region: the last animation started is the one that
145
+ should win, and `stopAnimation(widget, property)` exists for exactly that.
146
+
147
+ FletBox has no reduced-motion switch. That is not an excuse: build the screen so it
148
+ reads with zero animation, then add motion as a decision you can point at. If the
149
+ motion is only decoration, the honest answer to "respect reduced motion" is to not
150
+ add it.
151
+
152
+ ## M8 Clean up
153
+
154
+ An animation is a resource. `animate()` returns a cancel function — hold it and call
155
+ it when the widget unmounts, or call `stopAnimation(widget, property)`. A loop that
156
+ outlives its widget keeps a `requestAnimationFrame` alive and animates a detached
157
+ node forever. A widget that registers timers, listeners or observers releases them in
158
+ its `onUnmount` disposer.
159
+
160
+ ## M9 The check
161
+
162
+ 1. `npm run check` — every prop named exists (`flet-box unpack AnimatedBox`).
163
+ 2. `npm run ui`.
164
+ 3. Watch it, twice, at 375px. Then answer: how many times a day does a real user
165
+ see this? If the answer is "a lot", the correct fix is usually to delete the
166
+ animation, not to tune it.
167
+ 4. Check the frame, not the curve: press a control while something else moves. If
168
+ the press feels delayed, the motion is the cause.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: fletbox-rules
3
+ description: The working contract for a FletBox app - FletBox syntax only, never a widget or prop from React/Vue/Svelte/Flutter, look an API up before using it, the rem unit contract, prop aliases, key-based state, resource cleanup and the two gates that prove the work. Read first, before any other skill, and before writing or editing a single file in src/.
4
+ ---
5
+
6
+ # Rules — how to work inside this framework
7
+
8
+ AGENTS.md is the short version of this file. It exists because the models that
9
+ work here keep making the same five mistakes, and every one of them produces a
10
+ screen that renders and is quietly wrong.
11
+
12
+ ## R1 FletBox only — no other framework
13
+
14
+ A widget is a **function call that returns an element**. That is the whole model.
15
+ Nothing from another ecosystem is valid here, however close it looks:
16
+
17
+ ```js
18
+ // Correct: a function call with a props object.
19
+ Text({ text: 'Hello', size: 18, color: colors.text });
20
+
21
+ // Wrong: JSX. <Text size={18}>Hello</Text>
22
+ // Wrong: positional args. Text('Hello', 18);
23
+ // Wrong: a foreign import. import { Button } from 'react-native';
24
+ // Wrong: a framework that is not this one. <div class="…">, Svelte markup, Vue SFC.
25
+ ```
26
+
27
+ Every file is ESM (`"type": "module"`), imports come from `'flet-box'`, and there
28
+ is no build step to learn and no compiler to blame.
29
+
30
+ ## R2 Never invent an API — look it up
31
+
32
+ This is the rule the rest of the file exists to protect. A prop that silently does
33
+ nothing is worse than a compile error: it renders as a broken layout and nobody
34
+ finds it for weeks.
35
+
36
+ | I want to know… | Look here |
37
+ | --- | --- |
38
+ | Which widgets exist and which props each accepts | `.fletbox/api/cheatsheet.md` |
39
+ | Everything about one widget, with proof | `flet-box unpack <Widget>` |
40
+ | A call that is safe to paste | `flet-box unpack <Widget> --example` |
41
+ | Which props are documented but dead | `flet-box unpack <Widget> --diff` |
42
+ | The whole surface as data, to build calls from | `.fletbox/api/api.json` |
43
+ | Whether a name is exported at all | `npm run check -- --list` |
44
+
45
+ **If a name is not in those files, it does not exist.** The four places a prop can
46
+ live — `src/index.d.ts`, the prop registry, the widget's own source, its doc page —
47
+ do not always agree, and `unpack` labels every disagreement: `DEAD` (typed, routed
48
+ by nothing, read by nothing), `INVENTED` (read by the widget, declared nowhere),
49
+ `UNDOCUMENTED`. Design against what reaches the DOM.
50
+
51
+ - Do not edit `node_modules/flet-box`. It is a dependency, not source. A limitation
52
+ you hit there is fixed upstream, in the framework repo, or not at all.
53
+ - Regenerate the API files (`npm run api`) after upgrading the package; never
54
+ hand-edit them.
55
+ - When the API genuinely has no answer, choose a real composition or say the gap
56
+ out loud. Do not add a prop and hope the engine reads it — it drops unlisted
57
+ props.
58
+
59
+ ## R3 Aliases are the API, not a mistake
60
+
61
+ `bgColor` ≡ `backgroundColor`, `onPress` ≡ `onClick`, and the rest. They are
62
+ deliberate: each alias has exactly one meaning, resolved by the factory. Pick one
63
+ spelling per screen and keep it — but never "clean up" an alias out of the
64
+ framework, and never assume a second spelling is dead code.
65
+
66
+ ## R4 A number is not a pixel
67
+
68
+ Any length you hand a rem-routed prop (`padding`, `margin`, `gap`, `width`,
69
+ `size`, `borderRadius`, …) is divided by the base font size and emitted as `rem`,
70
+ so the whole interface grows when the reader raises their browser font. That is
71
+ the feature: one build fits phone, tablet and desktop.
72
+
73
+ ```js
74
+ Container({ padding: 16 }); // 1rem — 16px at the default base, more at a bigger one
75
+ ```
76
+
77
+ Pixels stay where pixels are honest: a canvas backing store, a value measured back
78
+ out of the DOM (`offsetWidth`, `getBoundingClientRect`, scroll offsets), the
79
+ hairline `borderWidth`, and a virtualised row's math in px space. Never write a
80
+ `'16px'` string for something that should scale.
81
+
82
+ ## R5 State, and one way to re-render
83
+
84
+ State is key-based RAM with subscribers: `useState('cart', 0)` returns the value
85
+ and a setter; the widgets bound to the key update themselves. Do not build a second
86
+ render system, do not reach into the DOM to change what a widget shows, and do not
87
+ manually rebuild a tree that a setter could update. Setters on a widget
88
+ (`widget.text = …`) are reactive for the same reason.
89
+
90
+ ## R6 Every resource you register, you release
91
+
92
+ A widget or a screen that adds a `window`/`document` listener, a timer, a
93
+ `requestAnimationFrame`, an observer, a router or storage subscription must release
94
+ it when it unmounts — one idempotent disposer in `onUnmount`. A leaked loop keeps
95
+ animating a detached node and outlives the screen that started it.
96
+
97
+ ## R7 One rendering engine
98
+
99
+ `WidgetFactory` turns props into an element (style, events, children, lifecycle,
100
+ reactivity). It is the only renderer. A custom component is still a function call
101
+ returning widgets; never add a parallel way to paint, and never monkey-patch
102
+ `container.update` — compose the update.
103
+
104
+ ## R8 The two gates, after every edit
105
+
106
+ ```bash
107
+ npm run check # does this API exist? exits 1 on an invented widget, prop,
108
+ # positional call, foreign import or broken module import
109
+ npm run ui # is this screen clean? tokens, the scale, tap targets, labels,
110
+ # contrast — and it cites the rule it broke
111
+ npm run fix # rewrites what the checker can prove: a wrong path, a default
112
+ # import of a named export, a missing import line, a typo in a
113
+ # prop key. Anything ambiguous is printed, never touched
114
+ ```
115
+
116
+ Then look at the screen at 375px and at 1440px, in both themes: **the checker has no
117
+ eyes.** A clean `npm run ui` says the values are legal, not that the screen is
118
+ understood.
119
+
120
+ A finding you keep needs one sentence of justification, written where the next
121
+ session will read it (R10). "The design wants it" is not a justification — it is a
122
+ rule that has not been written down yet.
123
+
124
+ ## R9 Do not trust your memory of this framework
125
+
126
+ FletBox has no `.d.ts` you have read in full, the prop set grows between versions,
127
+ and the installed version is the truth for this app. Three habits follow:
128
+
129
+ - Look up before using, even for `Text`.
130
+ - After a dependency bump, re-run `npm run check` across the app — it also reports
131
+ names the package stopped exporting.
132
+ - What is deployed is built from the **published** package, not from your working
133
+ tree. A fix you made in a local checkout does not exist for an installed app
134
+ until it is released.
135
+
136
+ ## R10 Write down what only you learned
137
+
138
+ The project keeps its memory in `.fletbox/memory/`. Read `MEMORY.md` before you
139
+ start, and record what the next session would otherwise rediscover the hard way.
140
+ `fletbox-memory` is the format and the rules; it is not optional — a session that
141
+ learns something and does not write it is a session that has to be repeated.
142
+
143
+ ## R11 The order the skills are read in
144
+
145
+ | # | Skill | Read it when |
146
+ | --- | --- | --- |
147
+ | 1 | `fletbox-rules` (this file) | always, before writing code |
148
+ | 2 | `fletbox-design` | creating, editing or reviewing any screen, form, list or dialog |
149
+ | 3 | `fletbox-motion` | adding any transition, entrance, pulse, parallax or hover lift |
150
+ | 4 | `fletbox-memory` | at the start and at the end of every session, and whenever progress changes |
151
+
152
+ They cover the same screen from three angles and do not repeat each other. When
153
+ they disagree, this file wins about the API, `fletbox-design` wins about the
154
+ screen, and the checker wins about a value.
@@ -103,6 +103,22 @@ export const palettes = {
103
103
  infoStrong: "#1d4ed8",
104
104
  infoPlate: "#1d4ed8",
105
105
  info: "#3b82f6",
106
+ // The alert tints, and the red the word "error" names. `check-ui` tells an app
107
+ // that "`colors.primaryBg`, `colors.errorBg` and friends are the tints" and the
108
+ // boot palette has had these five since the start, but the light palette did
109
+ // not — so an app that themed its alerts the framework way lost every tint the
110
+ // moment it booted into light (`colors.errorBg` came back `undefined`, and a
111
+ // `bgColor` of undefined is no background at all). Dark answered them first
112
+ // because a pale pink fill is unreadable on a black page and had to be
113
+ // invented; this is the same idea read from the light end: the tint is nearly
114
+ // white, so the ink on it is the `*Strong` of that hue — 5.91, 5.21, 4.84 and
115
+ // 6.16:1 in order, all over the AA line — and the page ink `#09090B` sits on
116
+ // all four at 18:1 and up.
117
+ error: "#ef4444",
118
+ errorBg: "#fef2f2",
119
+ successBg: "#ecfdf5",
120
+ warningBg: "#fffbeb",
121
+ infoBg: "#eff6ff",
106
122
  background: "#ffffff",
107
123
  surface: "#fafafa",
108
124
  card: "#ffffff",