flet-box 1.0.8 → 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.
- package/README.md +3 -0
- package/bin/commands/checkUi.js +53 -3
- package/bin/commands/create.js +44 -19
- package/bin/utils/skills.js +59 -0
- package/bin/utils/templates.js +143 -511
- package/docs/README.md +3 -1
- package/docs/cli/README.md +1 -1
- package/docs/guides/README.md +3 -1
- package/docs/guides/clean-ui.md +35 -17
- package/docs/guides/mobile-and-platforms.md +31 -0
- package/docs/tools/theme.md +1 -0
- package/docs/widget/README.md +2 -0
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +573 -0
- package/skills/fletbox-memory/SKILL.md +134 -0
- package/skills/fletbox-motion/SKILL.md +168 -0
- package/skills/fletbox-rules/SKILL.md +154 -0
- package/src/utils/themes.js +64 -4
|
@@ -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.
|
package/src/utils/themes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/components/flet-box/utils/themes.js
|
|
2
|
+
import { readableInk } from "./readableInk.js";
|
|
2
3
|
|
|
3
4
|
// ========== DEFAULT COLORS (static, safe) ==========
|
|
4
5
|
const DEFAULT_COLORS = {
|
|
@@ -102,6 +103,22 @@ export const palettes = {
|
|
|
102
103
|
infoStrong: "#1d4ed8",
|
|
103
104
|
infoPlate: "#1d4ed8",
|
|
104
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",
|
|
105
122
|
background: "#ffffff",
|
|
106
123
|
surface: "#fafafa",
|
|
107
124
|
card: "#ffffff",
|
|
@@ -247,6 +264,41 @@ const notifyThemeChange = () => {
|
|
|
247
264
|
};
|
|
248
265
|
|
|
249
266
|
// ========== THEME FUNCTIONS ==========
|
|
267
|
+
/**
|
|
268
|
+
* Moves the browser's own chrome — the PWA status bar and navigation bar, the
|
|
269
|
+
* scrollbar and form-control canvases — onto the palette that was just painted.
|
|
270
|
+
*
|
|
271
|
+
* An installed web app paints those bars from `<meta name="theme-color">`, which
|
|
272
|
+
* is a document-level value the page must rewrite itself: nothing in CSS reaches
|
|
273
|
+
* it. Left alone, the app keeps whatever literal the scaffold wrote — the indigo
|
|
274
|
+
* every FletBox PWA shipped before the zinc rebuild — and the reader sees a
|
|
275
|
+
* system bar from the old palette hanging over the new one. `color-scheme` is the
|
|
276
|
+
* same problem in the other direction: without it the OS draws light-mode
|
|
277
|
+
* scrollbars and form controls on a dark page.
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
*/
|
|
281
|
+
const syncBrowserChrome = () => {
|
|
282
|
+
try {
|
|
283
|
+
let meta = document.querySelector('meta[name="theme-color"]');
|
|
284
|
+
if (!meta) {
|
|
285
|
+
meta = document.createElement("meta");
|
|
286
|
+
meta.name = "theme-color";
|
|
287
|
+
document.head.appendChild(meta);
|
|
288
|
+
}
|
|
289
|
+
// The bars sit directly against the AppBar and the BottomNavigation, and
|
|
290
|
+
// both paint `surface` — matching `background` would put a band of the page
|
|
291
|
+
// color between two panels and look like a gap in the chrome.
|
|
292
|
+
meta.setAttribute("content", colors.surface);
|
|
293
|
+
// Whether the page is dark is read from its own background, not from the
|
|
294
|
+
// palette's name: `registerPalette` lets an app call a dark scale anything.
|
|
295
|
+
document.documentElement.style.colorScheme =
|
|
296
|
+
readableInk(colors.background, "#ffffff") === "#ffffff" ? "dark" : "light";
|
|
297
|
+
} catch (e) {
|
|
298
|
+
// Non-browser environments have no chrome to sync.
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
250
302
|
/**
|
|
251
303
|
* Copies a registered palette onto the live `colors` object and refreshes the
|
|
252
304
|
* CSS custom properties. Shared by `setTheme` and `registerPalette`; it does not
|
|
@@ -275,6 +327,8 @@ const applyPalette = (themeName) => {
|
|
|
275
327
|
// Silently fail in non-browser environments
|
|
276
328
|
}
|
|
277
329
|
|
|
330
|
+
syncBrowserChrome();
|
|
331
|
+
|
|
278
332
|
notifyThemeChange();
|
|
279
333
|
return colors;
|
|
280
334
|
};
|
|
@@ -291,10 +345,16 @@ export const setTheme = (themeName) => {
|
|
|
291
345
|
// and a subscriber that reads `getTheme()` while it rebuilds (a toggle button
|
|
292
346
|
// labelling itself, a theme-aware screen) has to see the theme it is being
|
|
293
347
|
// rebuilt for, not the one being replaced.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
348
|
+
//
|
|
349
|
+
// `window` is the test, not a try/catch around the call: Node 22+ defines a
|
|
350
|
+
// global `localStorage` that only answers with a warning and then throws, so
|
|
351
|
+
// an import-time theme would print an ExperimentalWarning on every server run.
|
|
352
|
+
if (typeof window !== "undefined") {
|
|
353
|
+
try {
|
|
354
|
+
localStorage.setItem("fletbox-theme", themeName);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
// Silently fail
|
|
357
|
+
}
|
|
298
358
|
}
|
|
299
359
|
|
|
300
360
|
applyPalette(themeName);
|