flet-box 1.0.6 → 1.0.8
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 +28 -1
- package/bin/cli.js +90 -11
- package/bin/commands/auditFix.js +682 -0
- package/bin/commands/checkApi.js +20 -6
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +86 -12
- package/bin/commands/createBundle.js +75 -1
- package/bin/commands/images.js +349 -0
- package/bin/commands/pruneAssets.js +170 -0
- package/bin/commands/release.js +34 -1
- package/bin/commands/runCloudflare.js +47 -9
- package/bin/commands/runServer.js +91 -25
- package/bin/commands/toPng.js +346 -0
- package/bin/commands/unpack.js +1173 -0
- package/bin/ota/loader.js +90 -0
- package/bin/utils/appSources.js +101 -0
- package/bin/utils/bundleConfig.js +166 -0
- package/bin/utils/dtsProps.js +147 -0
- package/bin/utils/imageTools.js +271 -0
- package/bin/utils/images/bmp.js +99 -0
- package/bin/utils/images/gif.js +244 -0
- package/bin/utils/images/index.js +163 -0
- package/bin/utils/images/jpeg.js +643 -0
- package/bin/utils/images/png.js +486 -0
- package/bin/utils/images/unsupported.js +29 -0
- package/bin/utils/neutralise.js +109 -6
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1653 -471
- package/docs/README.md +2 -0
- package/docs/cli/README.md +144 -1
- package/docs/guides/README.md +2 -0
- package/docs/guides/clean-ui.md +211 -0
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/online-updates.md +105 -9
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +1 -1
- package/docs/tools/markdown.md +3 -3
- package/docs/tools/theme.md +41 -1
- package/docs/widget/Accordion.md +4 -0
- package/docs/widget/AdMob.md +5 -1
- package/docs/widget/AdSense.md +4 -0
- package/docs/widget/AnimatedBox.md +7 -3
- package/docs/widget/AnimatedText.md +4 -0
- package/docs/widget/Audio.md +5 -1
- package/docs/widget/Avatar.md +15 -3
- package/docs/widget/Badge.md +4 -0
- package/docs/widget/Button.md +4 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +4 -0
- package/docs/widget/Carousel.md +4 -0
- package/docs/widget/Chart.md +4 -0
- package/docs/widget/Checkbox.md +4 -0
- package/docs/widget/Chip.md +4 -0
- package/docs/widget/CircularBar.md +4 -0
- package/docs/widget/CircularChart.md +4 -0
- package/docs/widget/CodeViewer.md +4 -0
- package/docs/widget/Column.md +4 -0
- package/docs/widget/Container.md +4 -0
- package/docs/widget/DataTable.md +4 -0
- package/docs/widget/Divider.md +4 -0
- package/docs/widget/DraggBox.md +6 -2
- package/docs/widget/Dropdown.md +6 -0
- package/docs/widget/DroppBox.md +5 -1
- package/docs/widget/FloatingActionButton.md +4 -0
- package/docs/widget/GridView.md +4 -0
- package/docs/widget/Icon.md +4 -0
- package/docs/widget/Image.md +14 -3
- package/docs/widget/Input.md +4 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +4 -0
- package/docs/widget/ListTile.md +4 -0
- package/docs/widget/ListView.md +4 -0
- package/docs/widget/Maps.md +191 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/MatrixRain.md +4 -0
- package/docs/widget/Pagination.md +4 -0
- package/docs/widget/ParallaxBox.md +4 -0
- package/docs/widget/ProgressBar.md +4 -0
- package/docs/widget/QRCode.md +4 -0
- package/docs/widget/README.md +12 -10
- package/docs/widget/Radio.md +5 -1
- package/docs/widget/Rating.md +4 -0
- package/docs/widget/Row.md +4 -0
- package/docs/widget/Skeleton.md +4 -0
- package/docs/widget/Slider.md +4 -0
- package/docs/widget/Stack.md +4 -0
- package/docs/widget/Stepper.md +4 -0
- package/docs/widget/Switch.md +4 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +4 -0
- package/docs/widget/Tooltip.md +14 -13
- package/docs/widget/TreeView.md +4 -0
- package/docs/widget/Video.md +6 -2
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/core/hmr-client.js +129 -34
- package/src/index.d.ts +195 -7
- package/src/index.js +2 -0
- package/src/navigations/AppBar.js +22 -16
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +25 -15
- package/src/navigations/DrawerItem.js +3 -1
- package/src/navigations/Scaffold.js +26 -12
- package/src/navigations/Tabs.js +104 -59
- package/src/tools/color.js +54 -0
- package/src/tools/index.js +9 -1
- package/src/tools/scale.js +41 -0
- package/src/tools/shadow.js +20 -0
- package/src/utils/index.js +1 -0
- package/src/utils/readableInk.js +19 -0
- package/src/utils/syntaxHighlight.js +78 -10
- package/src/utils/themes.js +217 -54
- package/src/utils/units.js +33 -11
- package/src/widget-factory/effects.js +56 -5
- package/src/widget-factory/processProps.js +10 -1
- package/src/widget-factory/remTool.js +15 -29
- package/src/widget-factory/stackPosition.js +18 -3
- package/src/widget-factory/translateProps.js +60 -1
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +236 -133
- package/src/widgets/Badge.js +3 -2
- package/src/widgets/BottomSheet.js +14 -13
- package/src/widgets/Button.js +69 -30
- package/src/widgets/Card.js +6 -2
- package/src/widgets/Carousel.js +12 -10
- package/src/widgets/Chart.js +28 -5
- package/src/widgets/Checkbox.js +76 -18
- package/src/widgets/Chip.js +31 -10
- package/src/widgets/CircularBar.js +7 -7
- package/src/widgets/CircularChart.js +26 -8
- package/src/widgets/CodeViewer.js +93 -20
- package/src/widgets/Container.js +13 -0
- package/src/widgets/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +59 -14
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- package/src/widgets/Image.js +102 -3
- package/src/widgets/Input.js +30 -23
- package/src/widgets/Inspector.js +54 -1
- package/src/widgets/InstallButton.js +6 -4
- package/src/widgets/ListTile.js +4 -3
- package/src/widgets/ListView.js +30 -12
- package/src/widgets/Maps.js +1495 -0
- package/src/widgets/Markdown.js +37 -26
- package/src/widgets/Modal.js +14 -11
- package/src/widgets/Pagination.js +16 -11
- package/src/widgets/ProgressBar.js +6 -4
- package/src/widgets/Radio.js +73 -15
- package/src/widgets/Rating.js +24 -6
- package/src/widgets/Skeleton.js +31 -31
- package/src/widgets/Slider.js +29 -18
- package/src/widgets/SnackBar.js +35 -39
- package/src/widgets/Stepper.js +13 -9
- package/src/widgets/Switch.js +59 -18
- package/src/widgets/Tooltip.js +24 -12
- package/src/widgets/TreeView.js +27 -15
- package/src/widgets/index.js +1 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// utils/syntaxHighlight.js
|
|
2
|
+
import { color } from "../tools/color.js";
|
|
3
|
+
import { getTheme } from "./themes.js";
|
|
2
4
|
|
|
3
5
|
// ========== FLETBOX COLORS ==========
|
|
4
6
|
const fletboxColors = {
|
|
@@ -118,22 +120,83 @@ const TokenType = {
|
|
|
118
120
|
PUNCTUATION: "punctuation",
|
|
119
121
|
FLETBOX_COLOR: "fletbox-color",
|
|
120
122
|
FLETBOX_WIDGET: "fletbox-widget",
|
|
123
|
+
/** Anything the tokenizer did not classify, e.g. `=`, `=>`, `+`. */
|
|
124
|
+
TEXT: "text",
|
|
121
125
|
};
|
|
122
126
|
|
|
123
127
|
// ========== COLORS PER TOKEN ==========
|
|
124
|
-
|
|
128
|
+
// Two palettes, chosen by the surface the code sits on. One Dark's values were
|
|
129
|
+
// designed for a dark pane and measure 1.6–2.3:1 on the light theme's #F4F4F5
|
|
130
|
+
// code block — the green string and the amber widget were the worst offenders.
|
|
131
|
+
// The light map is GitHub's light scale, which clears AA on that grey.
|
|
132
|
+
const DARK_HIGHLIGHT_COLORS = {
|
|
125
133
|
[TokenType.KEYWORD]: "#c678dd", // purple
|
|
126
134
|
[TokenType.STRING]: "#98c379", // green
|
|
127
135
|
[TokenType.NUMBER]: "#d19a66", // orange
|
|
128
|
-
|
|
136
|
+
// One Dark's #5C6370 measures 2.73:1 on the dark code block; a comment still
|
|
137
|
+
// has to be read, so the grey is the one GitHub paints comments with.
|
|
138
|
+
[TokenType.COMMENT]: "#8b949e",
|
|
129
139
|
[TokenType.PROPERTY]: "#e06c75", // red
|
|
130
140
|
[TokenType.FUNCTION]: "#61afef", // blue
|
|
131
141
|
[TokenType.BRACKET]: "#abb2bf", // light gray
|
|
132
142
|
[TokenType.PUNCTUATION]: "#abb2bf", // light gray
|
|
133
143
|
[TokenType.FLETBOX_COLOR]: "#56b6c2", // cyan
|
|
134
144
|
[TokenType.FLETBOX_WIDGET]: "#e5c07b", // yellow
|
|
145
|
+
// Raw characters used to inherit the page ink, which vanishes when the pane
|
|
146
|
+
// is painted in the opposite theme. They now carry the pane's own ink.
|
|
147
|
+
[TokenType.TEXT]: "#fafafa",
|
|
148
|
+
/** Muted ink for line numbers and similar chrome (not a token colour). */
|
|
149
|
+
muted: "#a1a1aa",
|
|
135
150
|
};
|
|
136
151
|
|
|
152
|
+
const LIGHT_HIGHLIGHT_COLORS = {
|
|
153
|
+
[TokenType.KEYWORD]: "#cf222e", // red
|
|
154
|
+
[TokenType.STRING]: "#0a3069", // navy
|
|
155
|
+
[TokenType.NUMBER]: "#0550ae", // blue
|
|
156
|
+
[TokenType.COMMENT]: "#57606a", // gray
|
|
157
|
+
[TokenType.PROPERTY]: "#953800", // brown
|
|
158
|
+
[TokenType.FUNCTION]: "#8250df", // purple
|
|
159
|
+
[TokenType.BRACKET]: "#57606a", // gray
|
|
160
|
+
[TokenType.PUNCTUATION]: "#57606a", // gray
|
|
161
|
+
[TokenType.FLETBOX_COLOR]: "#0e7490", // teal
|
|
162
|
+
[TokenType.FLETBOX_WIDGET]: "#116329", // green
|
|
163
|
+
[TokenType.TEXT]: "#09090b",
|
|
164
|
+
muted: "#52525b",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const highlightPalettes = {
|
|
168
|
+
light: LIGHT_HIGHLIGHT_COLORS,
|
|
169
|
+
dark: DARK_HIGHLIGHT_COLORS,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The map the highlighter exported before it became theme-aware. It is the dark
|
|
174
|
+
* palette, and it is still what a caller receives when it passes none.
|
|
175
|
+
*/
|
|
176
|
+
export const highlightColors = DARK_HIGHLIGHT_COLORS;
|
|
177
|
+
|
|
178
|
+
const SOLID_BACKGROUND = /^(#[0-9a-f]{3,8}|rgba?\(\s*\d)/i;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Picks the syntax palette for the surface the code is painted on, by that
|
|
182
|
+
* surface's own lightness. A dark block on the light theme still gets readable
|
|
183
|
+
* spans, and the widget default (`colors.gray100`) lands on the right map in
|
|
184
|
+
* both themes. Falls back to the active theme when the background is not a
|
|
185
|
+
* solid colour this module can measure (a gradient, a CSS variable) — guessing
|
|
186
|
+
* there is worse than the theme's answer.
|
|
187
|
+
* @param {string} [background] - CSS background colour of the code block.
|
|
188
|
+
* @returns {Record<string, string>} Token colour map.
|
|
189
|
+
*/
|
|
190
|
+
export const syntaxPaletteFor = (background) => {
|
|
191
|
+
if (typeof background === "string" && SOLID_BACKGROUND.test(background)) {
|
|
192
|
+
return color(background).isDark()
|
|
193
|
+
? DARK_HIGHLIGHT_COLORS
|
|
194
|
+
: LIGHT_HIGHLIGHT_COLORS;
|
|
195
|
+
}
|
|
196
|
+
return highlightPalettes[getTheme()] || DARK_HIGHLIGHT_COLORS;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
|
|
137
200
|
// ========== DETECT FLETBOX COLORS ==========
|
|
138
201
|
/**
|
|
139
202
|
* Returns `true` when the given word represents a FletBox color token.
|
|
@@ -284,7 +347,7 @@ export const tokenize = (code) => {
|
|
|
284
347
|
}
|
|
285
348
|
|
|
286
349
|
// ========== ANY OTHER CHARACTER ==========
|
|
287
|
-
tokens.push({ type:
|
|
350
|
+
tokens.push({ type: TokenType.TEXT, value: char });
|
|
288
351
|
i++;
|
|
289
352
|
}
|
|
290
353
|
|
|
@@ -294,12 +357,17 @@ export const tokenize = (code) => {
|
|
|
294
357
|
// ========== GENERATE HTML WITH COLORS ==========
|
|
295
358
|
/**
|
|
296
359
|
* Tokenizes `code` and wraps each token in a `<span>` with an inline `color`
|
|
297
|
-
* style
|
|
298
|
-
*
|
|
360
|
+
* style from `palette`. Whitespace and unrecognized tokens are emitted without
|
|
361
|
+
* a wrapper.
|
|
299
362
|
* @param {string} code - Source code to highlight.
|
|
363
|
+
* @param {Record<string, string>} [palette] - Token colour map; defaults to the
|
|
364
|
+
* palette of the active theme.
|
|
300
365
|
* @returns {string} HTML string safe for insertion into `innerHTML`.
|
|
301
366
|
*/
|
|
302
|
-
export const generateHighlightedHtml = (
|
|
367
|
+
export const generateHighlightedHtml = (
|
|
368
|
+
code,
|
|
369
|
+
palette = syntaxPaletteFor(),
|
|
370
|
+
) => {
|
|
303
371
|
const tokens = tokenize(code);
|
|
304
372
|
let html = "";
|
|
305
373
|
|
|
@@ -307,9 +375,9 @@ export const generateHighlightedHtml = (code) => {
|
|
|
307
375
|
if (token.type === "whitespace") {
|
|
308
376
|
html += token.value;
|
|
309
377
|
} else {
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
html += `<span style="color: ${
|
|
378
|
+
const tokenColor = palette[token.type];
|
|
379
|
+
if (tokenColor) {
|
|
380
|
+
html += `<span style="color: ${tokenColor};">${escapeHtml(token.value)}</span>`;
|
|
313
381
|
} else {
|
|
314
382
|
html += escapeHtml(token.value);
|
|
315
383
|
}
|
|
@@ -336,4 +404,4 @@ const escapeHtml = (text) => {
|
|
|
336
404
|
.replace(/'/g, "'");
|
|
337
405
|
};
|
|
338
406
|
|
|
339
|
-
export default { tokenize, generateHighlightedHtml, highlightColors };
|
|
407
|
+
export default { tokenize, generateHighlightedHtml, highlightColors, highlightPalettes, syntaxPaletteFor };
|
package/src/utils/themes.js
CHANGED
|
@@ -2,11 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
// ========== DEFAULT COLORS (static, safe) ==========
|
|
4
4
|
const DEFAULT_COLORS = {
|
|
5
|
-
|
|
5
|
+
// #6366f1 put white on 4.47:1 — a hair under AA for a button label. Two units
|
|
6
|
+
// darker measures 4.55:1, the same indigo, and every brand button passes.
|
|
7
|
+
primary: "#6165ef",
|
|
6
8
|
secondary: "#8b5cf6",
|
|
7
9
|
success: "#10b981",
|
|
8
10
|
warning: "#f59e0b",
|
|
11
|
+
// The readable greens and ambers, same rule as the red below: #10b981 and
|
|
12
|
+
// #f59e0b carry white at 2.54:1 and 2.15:1, so they are dots and borders. These
|
|
13
|
+
// are the ones a person can read or press — 5.48:1 and 5.02:1, both directions.
|
|
14
|
+
successStrong: "#047857",
|
|
15
|
+
warningStrong: "#b45309",
|
|
16
|
+
// #ef4444 is the error *signal*: a badge, an icon, a border. As words on a
|
|
17
|
+
// white page it measures 3.76:1, which is not AA, and white on it is 3.76 too.
|
|
9
18
|
danger: "#ef4444",
|
|
19
|
+
// The red a person can read and press: 6.47:1 against white in both directions,
|
|
20
|
+
// so it is legal as the label of a delete and as the plate under white words.
|
|
21
|
+
dangerStrong: "#b91c1c",
|
|
22
|
+
// `*Strong` answers "what may I write words in?" and its answer moves with the
|
|
23
|
+
// theme; `*Plate` answers "what may I fill, with white words on it?" and stays
|
|
24
|
+
// deep everywhere. On a light page the two are the same red — #b91c1c reads at
|
|
25
|
+
// 6.47:1 and carries white at 6.47:1 — so the light values below coincide with
|
|
26
|
+
// the Strong ones on purpose. On a dark page they part: the readable red is
|
|
27
|
+
// light, the plate behind white has to be dark.
|
|
28
|
+
successPlate: "#047857",
|
|
29
|
+
warningPlate: "#b45309",
|
|
30
|
+
dangerPlate: "#b91c1c",
|
|
31
|
+
// `info` had no readable sibling and it showed: a filled chip, a selected page
|
|
32
|
+
// or a tooltip that asks for `colors.info` paints white on #3b82f6, which is
|
|
33
|
+
// 3.68:1. This is the blue that carries the label — 6.70:1 in both directions.
|
|
34
|
+
infoStrong: "#1d4ed8",
|
|
35
|
+
infoPlate: "#1d4ed8",
|
|
10
36
|
info: "#3b82f6",
|
|
11
37
|
background: "#ffffff",
|
|
12
38
|
surface: "#f8fafc",
|
|
@@ -14,6 +40,19 @@ const DEFAULT_COLORS = {
|
|
|
14
40
|
text: "#0f172a",
|
|
15
41
|
textSecondary: "#64748b",
|
|
16
42
|
textDisabled: "#94a3b8",
|
|
43
|
+
// What to write on a `primary` fill. White measures 4.55:1 on #6165ef.
|
|
44
|
+
onPrimary: "#ffffff",
|
|
45
|
+
// What to write on a `secondary` fill (badges, tooltips). Neutral secondary is
|
|
46
|
+
// a dark plate in both themes, so its ink is white as well.
|
|
47
|
+
onSecondary: "#ffffff",
|
|
48
|
+
// The soft brand tint a card or icon chip uses instead of `primary + '15'`.
|
|
49
|
+
primaryBg: "#eef2ff",
|
|
50
|
+
// Glass is a translucent surface meant to sit over content with
|
|
51
|
+
// `backdropFilter`. It is a token and not an `rgba()` in the app for the same
|
|
52
|
+
// reason as every other color: the theme has to be able to move it, and the
|
|
53
|
+
// app's clean-UI gate rejects a hand-written literal (`RAW_COLOR`).
|
|
54
|
+
glass: "rgba(255, 255, 255, 0.66)",
|
|
55
|
+
glassBorder: "rgba(255, 255, 255, 0.75)",
|
|
17
56
|
border: "#e2e8f0",
|
|
18
57
|
gray50: "#f8fafc",
|
|
19
58
|
gray100: "#f1f5f9",
|
|
@@ -41,58 +80,133 @@ export let colors = { ...DEFAULT_COLORS };
|
|
|
41
80
|
// ========== PALETTES ==========
|
|
42
81
|
export const palettes = {
|
|
43
82
|
light: {
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
// Chakra's neutral scale: a white page, #09090B ink, #E4E4E7 hairlines,
|
|
84
|
+
// #FAFAFA panels and a near-black solid for everything selected or primary.
|
|
85
|
+
// `primary` is the plate under a checked box, an active page, a filled chip
|
|
86
|
+
// and the slider fill, so it has to be dark enough to carry its label:
|
|
87
|
+
// #18181B measures 17.3:1 with white, and reads as ink on the page too.
|
|
88
|
+
primary: "#18181b",
|
|
89
|
+
secondary: "#52525b",
|
|
46
90
|
success: "#10b981",
|
|
47
91
|
warning: "#f59e0b",
|
|
48
92
|
danger: "#ef4444",
|
|
93
|
+
dangerStrong: "#b91c1c",
|
|
94
|
+
successStrong: "#047857",
|
|
95
|
+
warningStrong: "#b45309",
|
|
96
|
+
// On a white page the readable red and the plate under white words are the
|
|
97
|
+
// same red, so the plates repeat the Strong values rather than inventing a
|
|
98
|
+
// fourth tone that no one would notice the difference from.
|
|
99
|
+
dangerPlate: "#b91c1c",
|
|
100
|
+
successPlate: "#047857",
|
|
101
|
+
warningPlate: "#b45309",
|
|
102
|
+
infoStrong: "#1d4ed8",
|
|
103
|
+
infoPlate: "#1d4ed8",
|
|
49
104
|
info: "#3b82f6",
|
|
50
105
|
background: "#ffffff",
|
|
51
|
-
surface: "#
|
|
106
|
+
surface: "#fafafa",
|
|
52
107
|
card: "#ffffff",
|
|
53
|
-
text: "#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
108
|
+
text: "#09090b",
|
|
109
|
+
// What to write on a `primary` fill. This is the theme's answer, not the
|
|
110
|
+
// widget's: `#18181B` is dark, so its ink is white.
|
|
111
|
+
onPrimary: "#ffffff",
|
|
112
|
+
// Badges and tooltips sit on `secondary` (#52525B), dark enough for white.
|
|
113
|
+
onSecondary: "#ffffff",
|
|
114
|
+
// The soft neutral tint a card or icon chip uses instead of `primary + '15'`.
|
|
115
|
+
primaryBg: "#f4f4f5",
|
|
116
|
+
glass: "rgba(255, 255, 255, 0.78)",
|
|
117
|
+
glassBorder: "rgba(9, 9, 11, 0.10)",
|
|
118
|
+
textSecondary: "#52525b",
|
|
119
|
+
textDisabled: "#a1a1aa",
|
|
120
|
+
border: "#e4e4e7",
|
|
121
|
+
gray50: "#fafafa",
|
|
122
|
+
gray100: "#f4f4f5",
|
|
123
|
+
gray200: "#e4e4e7",
|
|
124
|
+
gray300: "#d4d4d8",
|
|
125
|
+
gray400: "#a1a1aa",
|
|
126
|
+
gray500: "#71717a",
|
|
127
|
+
gray600: "#52525b",
|
|
128
|
+
gray700: "#3f3f46",
|
|
129
|
+
gray800: "#27272a",
|
|
130
|
+
gray900: "#18181b",
|
|
131
|
+
divider: "#e4e4e7",
|
|
132
|
+
overlay: "rgba(0, 0, 0, 0.5)",
|
|
133
|
+
shadow: "rgba(0, 0, 0, 0.08)",
|
|
69
134
|
},
|
|
70
135
|
dark: {
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
// The same zinc ladder read from the other end: #09090B page, #18181B cards
|
|
137
|
+
// and bars, #27272A hairlines, #A1A1AA secondary words. Every surface token
|
|
138
|
+
// is a true grey, so any widget that paints `surface`, `card`, `gray*` or
|
|
139
|
+
// `border` lands on the same black/grey/white hierarchy; only the semantic
|
|
140
|
+
// signals below keep a hue.
|
|
141
|
+
//
|
|
142
|
+
// `primary` is the inverse of the light theme: a near-white solid whose
|
|
143
|
+
// labels are the page ink (#09090B on #FAFAFA = 17.4:1). That is the same
|
|
144
|
+
// solid Chakra paints in dark mode, and it only works because a widget that
|
|
145
|
+
// writes on a primary fill asks the theme — `colors.onPrimary` — instead of
|
|
146
|
+
// assuming white.
|
|
147
|
+
primary: "#fafafa",
|
|
148
|
+
secondary: "#71717a",
|
|
73
149
|
success: "#10b981",
|
|
74
150
|
warning: "#fbbf24",
|
|
75
151
|
danger: "#f87171",
|
|
152
|
+
// On this page "the tone a person can read" is the light one, because the
|
|
153
|
+
// page is dark: #fca5a5 measures 9.33:1 on a card, 10.48:1 on the page, and
|
|
154
|
+
// the base signal #f87171 is already readable at 6.40:1. `*Strong` keeps its
|
|
155
|
+
// promise here — the red you may write words in — it just points the other
|
|
156
|
+
// way down the ladder than it does on a white page.
|
|
157
|
+
dangerStrong: "#fca5a5",
|
|
158
|
+
successStrong: "#34d399",
|
|
159
|
+
warningStrong: "#fcd34d",
|
|
160
|
+
// The deep plates are the tokens that used to hide inside `*Strong`. A badge
|
|
161
|
+
// or a completed step is a fill carrying white words, which on this page means
|
|
162
|
+
// going dark: white on #7f1d1d is 10.02:1, on #14532d 9.11:1, on #78350f 9.07:1.
|
|
163
|
+
dangerPlate: "#7f1d1d",
|
|
164
|
+
successPlate: "#14532d",
|
|
165
|
+
warningPlate: "#78350f",
|
|
166
|
+
// The same pair for the blue: #93c5fd is the word a person reads on this page
|
|
167
|
+
// (11.03:1), #1e40af is the plate a white label sits on (8.72:1).
|
|
168
|
+
infoStrong: "#93c5fd",
|
|
169
|
+
infoPlate: "#1e40af",
|
|
170
|
+
// The alert tints are a *fill*, and a pale pink fill is the wrong answer on a
|
|
171
|
+
// black page: `#fafafa` on `#ffe5e5` measures 1.14:1, so an error card that
|
|
172
|
+
// used to be painted from the boot value went unreadable in dark mode. These
|
|
173
|
+
// are the same deep tints a generated app's own palette declares, so an app
|
|
174
|
+
// that themes its alert the framework way and one that themes it by hand land
|
|
175
|
+
// on one number: the Strong ink of that hue sits on them at 6.04–10.51:1.
|
|
176
|
+
error: "#f87171",
|
|
177
|
+
errorBg: "#3b1113",
|
|
178
|
+
successBg: "#0c2f22",
|
|
179
|
+
warningBg: "#33230a",
|
|
180
|
+
infoBg: "#12263c",
|
|
76
181
|
info: "#60a5fa",
|
|
77
|
-
background: "#
|
|
78
|
-
surface: "#
|
|
79
|
-
card: "#
|
|
80
|
-
text: "#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
182
|
+
background: "#09090b",
|
|
183
|
+
surface: "#18181b",
|
|
184
|
+
card: "#18181b",
|
|
185
|
+
text: "#fafafa",
|
|
186
|
+
onPrimary: "#09090b",
|
|
187
|
+
// `secondary` stays a mid grey so badges and tooltips can carry white
|
|
188
|
+
// (4.6:1 on #71717A) while it still reads as ink on the surface.
|
|
189
|
+
onSecondary: "#ffffff",
|
|
190
|
+
primaryBg: "#27272a",
|
|
191
|
+
// A dark pane with a light rim, at the same alpha as the light token.
|
|
192
|
+
glass: "rgba(24, 24, 27, 0.72)",
|
|
193
|
+
glassBorder: "rgba(250, 250, 250, 0.12)",
|
|
194
|
+
textSecondary: "#a1a1aa",
|
|
195
|
+
textDisabled: "#52525b",
|
|
196
|
+
border: "#27272a",
|
|
197
|
+
gray50: "#111111",
|
|
198
|
+
gray100: "#1f1f1f",
|
|
199
|
+
gray200: "#27272a",
|
|
200
|
+
gray300: "#3f3f46",
|
|
201
|
+
gray400: "#52525b",
|
|
202
|
+
gray500: "#71717a",
|
|
203
|
+
gray600: "#a1a1aa",
|
|
204
|
+
gray700: "#d4d4d8",
|
|
205
|
+
gray800: "#e4e4e7",
|
|
206
|
+
gray900: "#fafafa",
|
|
207
|
+
divider: "#27272a",
|
|
208
|
+
overlay: "rgba(0, 0, 0, 0.72)",
|
|
209
|
+
shadow: "rgba(0, 0, 0, 0.6)",
|
|
96
210
|
},
|
|
97
211
|
};
|
|
98
212
|
|
|
@@ -134,13 +248,14 @@ const notifyThemeChange = () => {
|
|
|
134
248
|
|
|
135
249
|
// ========== THEME FUNCTIONS ==========
|
|
136
250
|
/**
|
|
137
|
-
*
|
|
138
|
-
* CSS custom properties
|
|
139
|
-
* and
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
251
|
+
* Copies a registered palette onto the live `colors` object and refreshes the
|
|
252
|
+
* CSS custom properties. Shared by `setTheme` and `registerPalette`; it does not
|
|
253
|
+
* persist anything, because "this is the palette now" and "the user picked this
|
|
254
|
+
* theme" are two different sentences.
|
|
255
|
+
* @param {string} themeName
|
|
256
|
+
* @returns {Object} the updated `colors` object
|
|
142
257
|
*/
|
|
143
|
-
|
|
258
|
+
const applyPalette = (themeName) => {
|
|
144
259
|
const palette = palettes[themeName];
|
|
145
260
|
if (palette) {
|
|
146
261
|
Object.keys(palette).forEach((key) => {
|
|
@@ -160,17 +275,58 @@ export const setTheme = (themeName) => {
|
|
|
160
275
|
// Silently fail in non-browser environments
|
|
161
276
|
}
|
|
162
277
|
|
|
163
|
-
|
|
278
|
+
notifyThemeChange();
|
|
279
|
+
return colors;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Applies a named theme palette to the global `colors` object, updates all
|
|
284
|
+
* CSS custom properties on `:root`, persists the choice to `localStorage`,
|
|
285
|
+
* and notifies all subscribers.
|
|
286
|
+
* @param {string} themeName - Name of the palette to apply (e.g. `"light"` or `"dark"`).
|
|
287
|
+
* @returns {Object} The updated `colors` object after the theme is applied.
|
|
288
|
+
*/
|
|
289
|
+
export const setTheme = (themeName) => {
|
|
290
|
+
// Persist before repainting: applyPalette notifies subscribers synchronously,
|
|
291
|
+
// and a subscriber that reads `getTheme()` while it rebuilds (a toggle button
|
|
292
|
+
// labelling itself, a theme-aware screen) has to see the theme it is being
|
|
293
|
+
// rebuilt for, not the one being replaced.
|
|
164
294
|
try {
|
|
165
295
|
localStorage.setItem("fletbox-theme", themeName);
|
|
166
296
|
} catch (e) {
|
|
167
297
|
// Silently fail
|
|
168
298
|
}
|
|
169
299
|
|
|
170
|
-
|
|
300
|
+
applyPalette(themeName);
|
|
171
301
|
return colors;
|
|
172
302
|
};
|
|
173
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Merges an app's own tokens into a named palette and, when that palette is the
|
|
306
|
+
* one currently on screen, repaints with it. An app that only declares a
|
|
307
|
+
* `colors` object changes nothing: every widget reads the framework's live
|
|
308
|
+
* `colors`, so the tokens have to be registered to reach the page.
|
|
309
|
+
*
|
|
310
|
+
* Unlike `setTheme` this never writes the theme preference — an app supplying
|
|
311
|
+
* its brand colors is not the user choosing light or dark.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} themeName - Palette to extend, usually `"light"` or `"dark"`.
|
|
314
|
+
* @param {Object} palette - Token names to override; unknown keys are ignored.
|
|
315
|
+
* @returns {Object} The registered palette.
|
|
316
|
+
*/
|
|
317
|
+
export const registerPalette = (themeName, palette) => {
|
|
318
|
+
if (!themeName || !palette || typeof palette !== "object") return palettes;
|
|
319
|
+
// A token the framework never heard of is seeded onto `colors` first:
|
|
320
|
+
// applyPalette only copies keys that already exist — that filter is what stops
|
|
321
|
+
// a palette which leaves one out (both built-ins do) from blanking it.
|
|
322
|
+
Object.keys(palette).forEach((key) => {
|
|
323
|
+
if (!colors.hasOwnProperty(key)) colors[key] = palette[key];
|
|
324
|
+
});
|
|
325
|
+
palettes[themeName] = { ...(palettes[themeName] || {}), ...palette };
|
|
326
|
+
if (getTheme() === themeName) applyPalette(themeName);
|
|
327
|
+
return palettes[themeName];
|
|
328
|
+
};
|
|
329
|
+
|
|
174
330
|
/**
|
|
175
331
|
* Returns the currently active theme name.
|
|
176
332
|
* Reads from `localStorage` first; falls back to the OS
|
|
@@ -215,7 +371,10 @@ export const applySystemTheme = () => {
|
|
|
215
371
|
.matches
|
|
216
372
|
? "dark"
|
|
217
373
|
: "light";
|
|
218
|
-
setTheme
|
|
374
|
+
// applyPalette, not setTheme: painting what the OS asked for is not the
|
|
375
|
+
// reader choosing a theme. Persisting here would pin the first answer and
|
|
376
|
+
// make watchSystemTheme's own savedTheme check bail forever.
|
|
377
|
+
applyPalette(systemTheme);
|
|
219
378
|
return systemTheme;
|
|
220
379
|
}
|
|
221
380
|
return savedTheme;
|
|
@@ -236,8 +395,9 @@ export const watchSystemTheme = () => {
|
|
|
236
395
|
const handler = (e) => {
|
|
237
396
|
const savedTheme = localStorage.getItem("fletbox-theme");
|
|
238
397
|
if (!savedTheme) {
|
|
239
|
-
|
|
240
|
-
|
|
398
|
+
// Same rule as applySystemTheme: the OS changing its mind is not the
|
|
399
|
+
// reader choosing a theme, so it must not be written down as one.
|
|
400
|
+
applyPalette(e.matches ? "dark" : "light");
|
|
241
401
|
}
|
|
242
402
|
};
|
|
243
403
|
mediaQuery.addEventListener("change", handler);
|
|
@@ -312,13 +472,16 @@ const initTheme = () => {
|
|
|
312
472
|
try {
|
|
313
473
|
const savedTheme = localStorage.getItem("fletbox-theme");
|
|
314
474
|
if (savedTheme && (savedTheme === "light" || savedTheme === "dark")) {
|
|
315
|
-
|
|
475
|
+
applyPalette(savedTheme);
|
|
316
476
|
} else {
|
|
317
477
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
|
318
478
|
.matches
|
|
319
479
|
? "dark"
|
|
320
480
|
: "light";
|
|
321
|
-
setTheme
|
|
481
|
+
// applyPalette, not setTheme: this runs the first time the module is
|
|
482
|
+
// imported, so persisting here wrote the OS answer down as a reader
|
|
483
|
+
// choice — and every later watcher then refused to follow the OS.
|
|
484
|
+
applyPalette(systemTheme);
|
|
322
485
|
}
|
|
323
486
|
watchSystemTheme();
|
|
324
487
|
} catch (e) {
|
package/src/utils/units.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
// toREM(24) // "1.5rem"
|
|
10
10
|
// toREM(8) // "0.5rem"
|
|
11
11
|
//
|
|
12
|
-
// //
|
|
13
|
-
// toREM('16px')
|
|
14
|
-
// toREM('
|
|
15
|
-
// toREM('
|
|
12
|
+
// // A length written in pixels is re-written in rem; anything else is left alone
|
|
13
|
+
// toREM('16px') // "1rem"
|
|
14
|
+
// toREM('8px 12px') // "0.5rem 0.75rem"
|
|
15
|
+
// toREM('1rem') // "1rem"
|
|
16
|
+
// toREM('50%') // "50%"
|
|
17
|
+
// toREM('2px solid red')// "2px solid red"
|
|
16
18
|
//
|
|
17
19
|
// // For exact pixels
|
|
18
20
|
// toPX(16) // "16px"
|
|
@@ -35,15 +37,30 @@ export const setBaseFontSize = (size) => {
|
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Converts a numeric value to rem, or keeps strings as they are
|
|
40
|
+
*
|
|
41
|
+
* A number always becomes rem: `40` is `2.5rem`, so it grows with the reader's
|
|
42
|
+
* base font the same way the rest of the page does.
|
|
43
|
+
*
|
|
44
|
+
* A string is usually a CSS value the framework should not touch (`"50%"`,
|
|
45
|
+
* `"calc(100% - 8px)"`, `"2px solid red"`) and passes through unchanged. The one
|
|
46
|
+
* exception is a length shorthand made only of pixels — `"8px 12px"`, `"0 16px"`,
|
|
47
|
+
* `"-4px -8px"`. That shape is a widget writing a framework number by hand
|
|
48
|
+
* (`padding: \`${v}px ${h}px\``), and if it passes through, the whole rule set
|
|
49
|
+
* still scales except for the one line that spelled its units out. So every
|
|
50
|
+
* token of it is converted: `"8px 12px"` → `"0.5rem 0.75rem"`.
|
|
51
|
+
*
|
|
38
52
|
* @param {number|string} value - Value to convert
|
|
39
|
-
* @returns {string} Converted value (e.g. "1rem", "
|
|
53
|
+
* @returns {string} Converted value (e.g. "1rem", "0.5rem 0.75rem", "50%")
|
|
40
54
|
* @example
|
|
41
|
-
* toREM(16)
|
|
42
|
-
* toREM(8)
|
|
43
|
-
* toREM('
|
|
44
|
-
* toREM('
|
|
45
|
-
* toREM('50%')
|
|
55
|
+
* toREM(16) // "1rem"
|
|
56
|
+
* toREM(8) // "0.5rem"
|
|
57
|
+
* toREM('8px 12px')// "0.5rem 0.75rem"
|
|
58
|
+
* toREM('16px') // "1rem"
|
|
59
|
+
* toREM('50%') // "50%"
|
|
60
|
+
* toREM('2px solid red') // unchanged: "solid" is not a length
|
|
46
61
|
*/
|
|
62
|
+
const PX_TOKEN = /^-?\d+(?:\.\d+)?px$/;
|
|
63
|
+
|
|
47
64
|
export const toREM = (value) => {
|
|
48
65
|
if (value === undefined || value === null) return undefined;
|
|
49
66
|
|
|
@@ -54,7 +71,12 @@ export const toREM = (value) => {
|
|
|
54
71
|
|
|
55
72
|
// If it is a string, return it as-is
|
|
56
73
|
if (typeof value === "string") {
|
|
57
|
-
|
|
74
|
+
const parts = value.trim().split(/\s+/);
|
|
75
|
+
const allPixels = parts.every((part) => PX_TOKEN.test(part));
|
|
76
|
+
if (!allPixels) return value;
|
|
77
|
+
return parts
|
|
78
|
+
.map((part) => `${parseFloat(part) / BASE_FONT_SIZE}rem`)
|
|
79
|
+
.join(" ");
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
// For other types (objects, booleans, etc.)
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
// core/effects.js
|
|
2
2
|
|
|
3
|
+
import { SHADOW_LEVELS } from "../tools/shadow.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The three geometry numbers of a CSS shadow: x, y and blur.
|
|
7
|
+
* The colour is stripped first, because the scale writes `0 3px 6px` while the
|
|
8
|
+
* browser serialises the live style as `0px 3px 6px 0px` — matching numbers
|
|
9
|
+
* instead of tokens is what lets both forms compare equal.
|
|
10
|
+
*/
|
|
11
|
+
const shadowGeometry = (value) => {
|
|
12
|
+
const numbers = String(value ?? "")
|
|
13
|
+
.replace(/rgba?\([^)]*\)/g, "")
|
|
14
|
+
.trim()
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.map(parseFloat);
|
|
17
|
+
return numbers.length >= 3 && numbers.every(Number.isFinite)
|
|
18
|
+
? numbers.slice(0, 3)
|
|
19
|
+
: null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The next `SHADOW_LEVELS` entry above `value`, or `null` when the shadow is not
|
|
24
|
+
* one of the scale's levels. Matching is by geometry, not by string: the browser
|
|
25
|
+
* serialises "0 3px 6px rgba(0,0,0,0.16)" as "rgba(0, 0, 0, 0.16) 0px 3px 6px",
|
|
26
|
+
* so comparing the literals would never find a level.
|
|
27
|
+
*/
|
|
28
|
+
const nextElevation = (value) => {
|
|
29
|
+
const geometry = shadowGeometry(value);
|
|
30
|
+
if (!geometry) return null;
|
|
31
|
+
const level = Object.keys(SHADOW_LEVELS).find((key) => {
|
|
32
|
+
const candidate = shadowGeometry(SHADOW_LEVELS[key]);
|
|
33
|
+
return candidate && candidate.every((n, i) => Math.abs(n - geometry[i]) < 0.01);
|
|
34
|
+
});
|
|
35
|
+
if (level === undefined) return null;
|
|
36
|
+
return SHADOW_LEVELS[Number(level) + 1] ?? null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
|
|
3
40
|
/**
|
|
4
41
|
* Applies interactive visual effects to a widget that has a click handler.
|
|
5
42
|
*
|
|
@@ -19,7 +56,8 @@
|
|
|
19
56
|
* - On press end (`mouseup` / `touchend` / `mouseleave` / `touchcancel`):
|
|
20
57
|
* restores the original scale and shadow.
|
|
21
58
|
* - On hover enter (`mouseenter`): translates the widget up by `0.5px` and
|
|
22
|
-
*
|
|
59
|
+
* raises the box shadow by one `SHADOW_LEVELS` step when the widget already
|
|
60
|
+
* carries a shadow from that scale.
|
|
23
61
|
* - On hover leave (`mouseleave`): restores the original translate and shadow.
|
|
24
62
|
* - Sets `cursor: pointer`.
|
|
25
63
|
*
|
|
@@ -47,7 +85,12 @@ export const applyEffects = (widget) => {
|
|
|
47
85
|
if (!hasClick) return widget;
|
|
48
86
|
|
|
49
87
|
// Store the current shadow so it can be restored after press / hover effects.
|
|
50
|
-
|
|
88
|
+
//
|
|
89
|
+
// Skipped while the pointer is over the widget: `update()` re-runs
|
|
90
|
+
// `applyEffects`, and capturing the lifted hover shadow as the base would make
|
|
91
|
+
// every update climb one more elevation step.
|
|
92
|
+
if (!widget._hovering) widget._originalShadow = widget.style.boxShadow || "";
|
|
93
|
+
const hoverShadow = nextElevation(widget._originalShadow);
|
|
51
94
|
|
|
52
95
|
const disableTransform = widget._originalProps?.disableTransform === true;
|
|
53
96
|
const listeners = [];
|
|
@@ -98,11 +141,10 @@ export const applyEffects = (widget) => {
|
|
|
98
141
|
*/
|
|
99
142
|
const onHoverEnter = () => {
|
|
100
143
|
if (isPressed) return;
|
|
144
|
+
widget._hovering = true;
|
|
101
145
|
// Use the independent `translate` property.
|
|
102
146
|
widget.style.translate = "0 -0.5px";
|
|
103
|
-
if (
|
|
104
|
-
widget.style.boxShadow = "0 8px 16px rgba(0,0,0,0.2)";
|
|
105
|
-
}
|
|
147
|
+
if (hoverShadow) widget.style.boxShadow = hoverShadow;
|
|
106
148
|
};
|
|
107
149
|
|
|
108
150
|
/**
|
|
@@ -115,6 +157,7 @@ export const applyEffects = (widget) => {
|
|
|
115
157
|
// Restore translate without touching transform.
|
|
116
158
|
widget.style.translate = "";
|
|
117
159
|
widget.style.boxShadow = widget._originalShadow || "";
|
|
160
|
+
widget._hovering = false;
|
|
118
161
|
};
|
|
119
162
|
|
|
120
163
|
widget.addEventListener("mouseenter", onHoverEnter, { passive: true });
|
|
@@ -122,6 +165,14 @@ export const applyEffects = (widget) => {
|
|
|
122
165
|
listeners.push({ event: "mouseenter", handler: onHoverEnter });
|
|
123
166
|
listeners.push({ event: "mouseleave", handler: onHoverLeave });
|
|
124
167
|
|
|
168
|
+
// An update re-applies the props' own shadow and translate, which cancels a
|
|
169
|
+
// hover that is still in progress. Re-assert it, from the same base, so a
|
|
170
|
+
// reactive update neither drops the lift nor climbs another step.
|
|
171
|
+
if (widget._hovering) {
|
|
172
|
+
widget.style.translate = "0 -0.5px";
|
|
173
|
+
if (hoverShadow) widget.style.boxShadow = hoverShadow;
|
|
174
|
+
}
|
|
175
|
+
|
|
125
176
|
widget.style.cursor = "pointer";
|
|
126
177
|
}
|
|
127
178
|
|