flet-box 1.0.7 → 1.0.9
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/cli.js +13 -0
- package/bin/commands/checkApi.js +13 -1
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +49 -5
- package/bin/commands/createBundle.js +21 -0
- package/bin/commands/runServer.js +11 -2
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1398 -443
- package/docs/README.md +4 -0
- package/docs/cli/README.md +63 -2
- 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/mobile-and-platforms.md +31 -0
- package/docs/guides/online-updates.md +48 -0
- 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 +42 -1
- package/docs/widget/Accordion.md +3 -0
- package/docs/widget/AdMob.md +4 -1
- package/docs/widget/AdSense.md +3 -0
- package/docs/widget/AnimatedBox.md +6 -3
- package/docs/widget/AnimatedText.md +3 -0
- package/docs/widget/Audio.md +3 -0
- package/docs/widget/Avatar.md +3 -0
- package/docs/widget/Badge.md +3 -0
- package/docs/widget/Button.md +3 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +3 -0
- package/docs/widget/Carousel.md +3 -0
- package/docs/widget/Chart.md +3 -0
- package/docs/widget/Checkbox.md +3 -0
- package/docs/widget/Chip.md +3 -0
- package/docs/widget/CircularBar.md +3 -0
- package/docs/widget/CircularChart.md +3 -0
- package/docs/widget/CodeViewer.md +3 -0
- package/docs/widget/Column.md +3 -0
- package/docs/widget/Container.md +3 -0
- package/docs/widget/DataTable.md +3 -0
- package/docs/widget/Divider.md +3 -0
- package/docs/widget/DraggBox.md +3 -0
- package/docs/widget/Dropdown.md +3 -0
- package/docs/widget/DroppBox.md +3 -0
- package/docs/widget/FloatingActionButton.md +3 -0
- package/docs/widget/GridView.md +3 -0
- package/docs/widget/Icon.md +3 -0
- package/docs/widget/Image.md +3 -0
- package/docs/widget/Input.md +3 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +3 -0
- package/docs/widget/ListTile.md +3 -0
- package/docs/widget/ListView.md +3 -0
- package/docs/widget/Maps.md +3 -0
- package/docs/widget/Markdown.md +3 -0
- package/docs/widget/MatrixRain.md +3 -0
- package/docs/widget/Pagination.md +3 -0
- package/docs/widget/ParallaxBox.md +3 -0
- package/docs/widget/ProgressBar.md +3 -0
- package/docs/widget/QRCode.md +3 -0
- package/docs/widget/README.md +2 -0
- package/docs/widget/Radio.md +4 -1
- package/docs/widget/Rating.md +3 -0
- package/docs/widget/Row.md +3 -0
- package/docs/widget/Skeleton.md +3 -0
- package/docs/widget/Slider.md +3 -0
- package/docs/widget/Stack.md +3 -0
- package/docs/widget/Stepper.md +3 -0
- package/docs/widget/Switch.md +3 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +3 -0
- package/docs/widget/Tooltip.md +3 -0
- package/docs/widget/TreeView.md +3 -0
- package/docs/widget/Video.md +3 -0
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/index.d.ts +64 -8
- package/src/index.js +1 -0
- package/src/navigations/AppBar.js +7 -12
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +16 -14
- 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 +265 -58
- 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 +28 -0
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +18 -12
- 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/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +26 -13
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- 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/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 +12 -10
- package/src/widgets/TreeView.js +27 -15
package/src/navigations/Tabs.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
3
3
|
import { colors } from "../utils/themes.js";
|
|
4
4
|
import { Container } from "../widgets/Container.js";
|
|
5
|
+
import { Column } from "../widgets/Column.js";
|
|
5
6
|
import { Row } from "../widgets/Row.js";
|
|
6
7
|
import { Text } from "../widgets/Text.js";
|
|
7
8
|
import { Icon } from "../widgets/Icon.js";
|
|
9
|
+
import { toREM } from "../utils/units.js";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* @typedef {Object} TabDefinition
|
|
@@ -66,14 +68,16 @@ export const Tabs = (props) => {
|
|
|
66
68
|
variant = "underline",
|
|
67
69
|
size = "medium",
|
|
68
70
|
color = colors.primary,
|
|
69
|
-
textColor = colors.
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
activeTextColor = colors.background,
|
|
71
|
+
textColor = colors.textSecondary,
|
|
72
|
+
// `background` made the active label the page color, so it vanished on a
|
|
73
|
+
// transparent tab bar (2.4:1 in light, 1.3:1 in dark). Resolved per variant
|
|
74
|
+
// below: the slider sits on a secondary thumb and needs the ink color.
|
|
75
|
+
activeTextColor,
|
|
75
76
|
bgColor = colors.gray100,
|
|
76
|
-
|
|
77
|
+
// The thumb carries the accent, and its label is the ink color in both
|
|
78
|
+
// themes. `secondary` missed AA on the light thumb (4.23:1); primary is
|
|
79
|
+
// 6.65:1 with ink, 6.68:1 in dark.
|
|
80
|
+
buttonColor = color,
|
|
77
81
|
alignment = "left",
|
|
78
82
|
fullWidth = true,
|
|
79
83
|
showDivider = true,
|
|
@@ -86,7 +90,7 @@ export const Tabs = (props) => {
|
|
|
86
90
|
} = props;
|
|
87
91
|
|
|
88
92
|
let currentIndex = activeIndex;
|
|
89
|
-
let
|
|
93
|
+
let activeIndicator = null;
|
|
90
94
|
let tabsWrapperRef = null;
|
|
91
95
|
let tabButtonsRef = [];
|
|
92
96
|
|
|
@@ -99,6 +103,16 @@ export const Tabs = (props) => {
|
|
|
99
103
|
};
|
|
100
104
|
const sz = sizes[size] || sizes.medium;
|
|
101
105
|
|
|
106
|
+
const resolvedActiveTextColor =
|
|
107
|
+
activeTextColor ?? (variant === "slider" ? colors.onPrimary : colors.text);
|
|
108
|
+
|
|
109
|
+
const tabBackground = (isActive) => {
|
|
110
|
+
if (!isActive) return "transparent";
|
|
111
|
+
if (variant === "pills") return `${color}1A`;
|
|
112
|
+
if (variant === "filled") return colors.card;
|
|
113
|
+
return "transparent";
|
|
114
|
+
};
|
|
115
|
+
|
|
102
116
|
/** Extract display label from a tab definition or plain string. */
|
|
103
117
|
const getLabel = (t) =>
|
|
104
118
|
typeof t === "object" ? t.label || t.title : String(t);
|
|
@@ -157,43 +171,64 @@ export const Tabs = (props) => {
|
|
|
157
171
|
const badge = getBadge(idx);
|
|
158
172
|
const isActive = currentIndex === idx;
|
|
159
173
|
|
|
160
|
-
// Build the
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
Text({
|
|
174
|
+
// Build the content inside each tab button. Every documented value of
|
|
175
|
+
// `iconPosition` keeps the icon: "left" puts it before the label, "right"
|
|
176
|
+
// after it, and "top"/"bottom" stack it above/below the label row.
|
|
177
|
+
const iconWidget =
|
|
178
|
+
showIcon && icon
|
|
179
|
+
? Icon({
|
|
180
|
+
name: icon,
|
|
181
|
+
size: iconSize || sz.i,
|
|
182
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
183
|
+
})
|
|
184
|
+
: null;
|
|
185
|
+
const labelWidget = label
|
|
186
|
+
? Text({
|
|
174
187
|
text: label,
|
|
175
188
|
size: sz.f,
|
|
176
|
-
color: isActive ?
|
|
189
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
177
190
|
weight: isActive ? "500" : "400",
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
content.push(
|
|
183
|
-
Container({
|
|
191
|
+
})
|
|
192
|
+
: null;
|
|
193
|
+
const badgeWidget = badge
|
|
194
|
+
? Container({
|
|
184
195
|
backgroundColor: colors.danger,
|
|
185
196
|
borderRadius: 10,
|
|
186
197
|
padding: "2px 6px",
|
|
187
198
|
marginLeft: 4,
|
|
188
199
|
child: Text({ text: String(badge), size: 10, color: "#fff" }),
|
|
189
|
-
})
|
|
190
|
-
|
|
200
|
+
})
|
|
201
|
+
: null;
|
|
202
|
+
|
|
203
|
+
const line = [labelWidget, badgeWidget].filter(Boolean);
|
|
204
|
+
if (iconWidget && iconPosition !== "top" && iconPosition !== "bottom") {
|
|
205
|
+
if (iconPosition === "left") line.unshift(iconWidget);
|
|
206
|
+
else line.push(iconWidget);
|
|
191
207
|
}
|
|
208
|
+
const labelRow = Row({
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "center",
|
|
211
|
+
gap: 4,
|
|
212
|
+
children: line,
|
|
213
|
+
});
|
|
214
|
+
const stacked =
|
|
215
|
+
iconWidget && (iconPosition === "top" || iconPosition === "bottom");
|
|
216
|
+
const content = stacked
|
|
217
|
+
? Column({
|
|
218
|
+
alignItems: "center",
|
|
219
|
+
justifyContent: "center",
|
|
220
|
+
gap: 4,
|
|
221
|
+
children:
|
|
222
|
+
iconPosition === "top"
|
|
223
|
+
? [iconWidget, labelRow]
|
|
224
|
+
: [labelRow, iconWidget],
|
|
225
|
+
})
|
|
226
|
+
: labelRow;
|
|
192
227
|
|
|
193
228
|
const btn = Container({
|
|
194
229
|
borderRadius: variant === "slider" ? 20 : variant === "pills" ? 20 : 0,
|
|
195
|
-
backgroundColor:
|
|
196
|
-
color: isActive ?
|
|
230
|
+
backgroundColor: tabBackground(isActive),
|
|
231
|
+
color: isActive ? resolvedActiveTextColor : textColor,
|
|
197
232
|
display: "flex",
|
|
198
233
|
alignItems: "center",
|
|
199
234
|
justifyContent: "center",
|
|
@@ -220,12 +255,7 @@ export const Tabs = (props) => {
|
|
|
220
255
|
}
|
|
221
256
|
}
|
|
222
257
|
},
|
|
223
|
-
child:
|
|
224
|
-
alignItems: "center",
|
|
225
|
-
justifyContent: "center",
|
|
226
|
-
gap: 4,
|
|
227
|
-
children: content,
|
|
228
|
-
}),
|
|
258
|
+
child: content,
|
|
229
259
|
});
|
|
230
260
|
|
|
231
261
|
btn.onclick = () => {
|
|
@@ -242,20 +272,34 @@ export const Tabs = (props) => {
|
|
|
242
272
|
tabButtonsRef = tabButtons;
|
|
243
273
|
|
|
244
274
|
// The slider variant uses an absolutely positioned "thumb" div that
|
|
245
|
-
// animates to the active tab's position via CSS transform.
|
|
246
|
-
|
|
247
|
-
|
|
275
|
+
// animates to the active tab's position via CSS transform. The default
|
|
276
|
+
// `underline` variant needs the same moving element: without it the active
|
|
277
|
+
// tab had no cue besides its label color.
|
|
278
|
+
const usesIndicator = variant === "slider" || variant === "underline";
|
|
279
|
+
if (usesIndicator) {
|
|
280
|
+
activeIndicator = WidgetFactory({
|
|
248
281
|
tag: "div",
|
|
249
282
|
position: "absolute",
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
...(variant === "slider"
|
|
284
|
+
? {
|
|
285
|
+
top: "4px",
|
|
286
|
+
left: "4px",
|
|
287
|
+
height: "calc(100% - 8px)",
|
|
288
|
+
backgroundColor: buttonColor,
|
|
289
|
+
borderRadius: 20,
|
|
290
|
+
}
|
|
291
|
+
: {
|
|
292
|
+
bottom: "0",
|
|
293
|
+
left: "0",
|
|
294
|
+
height: "2px",
|
|
295
|
+
backgroundColor: color,
|
|
296
|
+
borderRadius: "2px 2px 0 0",
|
|
297
|
+
}),
|
|
255
298
|
transition: "transform 0.3s ease, width 0.3s ease",
|
|
299
|
+
pointerEvents: "none",
|
|
256
300
|
zIndex: 1,
|
|
257
301
|
});
|
|
258
|
-
tabsWrapper.appendChild(
|
|
302
|
+
tabsWrapper.appendChild(activeIndicator);
|
|
259
303
|
}
|
|
260
304
|
|
|
261
305
|
tabBar.appendChild(tabsWrapper);
|
|
@@ -263,7 +307,7 @@ export const Tabs = (props) => {
|
|
|
263
307
|
|
|
264
308
|
const contentContainer = Container({
|
|
265
309
|
display: "flex",
|
|
266
|
-
padding: `${sz.cp}
|
|
310
|
+
padding: `${toREM(sz.cp)} 0`,
|
|
267
311
|
width: "100%",
|
|
268
312
|
overflow: "auto",
|
|
269
313
|
});
|
|
@@ -284,13 +328,13 @@ export const Tabs = (props) => {
|
|
|
284
328
|
* Reads live DOM rectangles so it stays accurate after layout changes.
|
|
285
329
|
*/
|
|
286
330
|
const updateSliderPosition = () => {
|
|
287
|
-
if (!
|
|
331
|
+
if (!activeIndicator || !tabButtonsRef[currentIndex]) return;
|
|
288
332
|
const btn = tabButtonsRef[currentIndex];
|
|
289
333
|
const wrapperRect = tabsWrapperRef.getBoundingClientRect();
|
|
290
334
|
const btnRect = btn.getBoundingClientRect();
|
|
291
335
|
const left = btnRect.left - wrapperRect.left;
|
|
292
|
-
|
|
293
|
-
|
|
336
|
+
activeIndicator.style.width = `${btnRect.width}px`;
|
|
337
|
+
activeIndicator.style.transform = `translateX(${left}px)`;
|
|
294
338
|
};
|
|
295
339
|
|
|
296
340
|
/**
|
|
@@ -308,14 +352,15 @@ export const Tabs = (props) => {
|
|
|
308
352
|
|
|
309
353
|
const textSpan = btn.querySelector("span:not(.material-icons)");
|
|
310
354
|
if (textSpan) {
|
|
311
|
-
textSpan.style.color = isActive ?
|
|
355
|
+
textSpan.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
312
356
|
textSpan.style.fontWeight = isActive ? "500" : "400";
|
|
313
357
|
}
|
|
314
358
|
const iconEl = btn.querySelector(".material-icons");
|
|
315
359
|
if (iconEl) {
|
|
316
|
-
iconEl.style.color = isActive ?
|
|
360
|
+
iconEl.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
317
361
|
}
|
|
318
|
-
btn.style.color = isActive ?
|
|
362
|
+
btn.style.color = isActive ? resolvedActiveTextColor : textColor;
|
|
363
|
+
btn.style.backgroundColor = tabBackground(isActive);
|
|
319
364
|
});
|
|
320
365
|
|
|
321
366
|
updateSliderPosition();
|
|
@@ -341,17 +386,17 @@ export const Tabs = (props) => {
|
|
|
341
386
|
const handleResize = () => {
|
|
342
387
|
clearTimeout(resizeTimeout);
|
|
343
388
|
resizeTimeout = setTimeout(() => {
|
|
344
|
-
if (
|
|
389
|
+
if (activeIndicator && tabButtonsRef[currentIndex]) {
|
|
345
390
|
updateSliderPosition();
|
|
346
391
|
}
|
|
347
392
|
}, 100);
|
|
348
393
|
};
|
|
349
394
|
window.addEventListener("resize", handleResize);
|
|
350
395
|
|
|
351
|
-
// Also observe the tab bar element itself so the
|
|
396
|
+
// Also observe the tab bar element itself so the indicator repositions when
|
|
352
397
|
// the container resizes independently of the window (e.g. sidebar toggle).
|
|
353
|
-
// Only
|
|
354
|
-
if (
|
|
398
|
+
// Only slider and underline render an indicator, so skip it for the others.
|
|
399
|
+
if (usesIndicator && typeof ResizeObserver !== "undefined") {
|
|
355
400
|
resizeObserver = new ResizeObserver(() => updateSliderPosition());
|
|
356
401
|
if (tabsWrapperRef) resizeObserver.observe(tabsWrapperRef);
|
|
357
402
|
}
|
package/src/tools/color.js
CHANGED
|
@@ -291,6 +291,60 @@ class Color {
|
|
|
291
291
|
return this.isDark() ? "#ffffff" : "#000000";
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Relative luminance of the color, as WCAG 2.1 defines it.
|
|
296
|
+
*
|
|
297
|
+
* Each channel is sRGB-decoded (the linear branch below 0.04045 is the one
|
|
298
|
+
* that matters for dark colors, where `isDark()`'s BT.601 luma is a poor
|
|
299
|
+
* proxy) and weighted 0.2126 / 0.7152 / 0.0722. Alpha is ignored: this is the
|
|
300
|
+
* luminance of the color itself, so a translucent color must be composited
|
|
301
|
+
* over its background before being measured.
|
|
302
|
+
*
|
|
303
|
+
* @returns {number} 0 for black, 1 for white
|
|
304
|
+
*/
|
|
305
|
+
luminance() {
|
|
306
|
+
const channel = (value) => {
|
|
307
|
+
const c = value / 255;
|
|
308
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
309
|
+
};
|
|
310
|
+
return (
|
|
311
|
+
0.2126 * channel(this.r) + 0.7152 * channel(this.g) + 0.0722 * channel(this.b)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Contrast ratio between this color and another, in WCAG terms.
|
|
317
|
+
*
|
|
318
|
+
* Returns the ratio of the lighter luminance to the darker one, from 1
|
|
319
|
+
* (identical) to 21 (black on white). This is what the readability thresholds
|
|
320
|
+
* are written against: 4.5 for body text, 3 for large text (>= 24px, or 19px
|
|
321
|
+
* bold) and for a control's own outline against the surface it sits on.
|
|
322
|
+
*
|
|
323
|
+
* @param {string|Color} other - The second color (any form `color()` parses).
|
|
324
|
+
* @returns {number} Contrast ratio, >= 1
|
|
325
|
+
* @example
|
|
326
|
+
* color('#64748b').contrastRatio('#ffffff'); // 4.76 — the textSecondary token, passes
|
|
327
|
+
* color('#94a3b8').contrastRatio('#ffffff'); // 2.56 — the textDisabled token, fails
|
|
328
|
+
*/
|
|
329
|
+
contrastRatio(other) {
|
|
330
|
+
const a = this.luminance();
|
|
331
|
+
const b = other instanceof Color ? other.luminance() : color(other).luminance();
|
|
332
|
+
const light = Math.max(a, b);
|
|
333
|
+
const dark = Math.min(a, b);
|
|
334
|
+
return (light + 0.05) / (dark + 0.05);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Whether this color and another clear a WCAG contrast threshold.
|
|
339
|
+
*
|
|
340
|
+
* @param {string|Color} other - The color it is read against.
|
|
341
|
+
* @param {number} [minimum=4.5] - The ratio required: 4.5 body text, 3 large text.
|
|
342
|
+
* @returns {boolean} `true` when the pair is at or above `minimum`
|
|
343
|
+
*/
|
|
344
|
+
meetsContrast(other, minimum = 4.5) {
|
|
345
|
+
return this.contrastRatio(other) >= minimum;
|
|
346
|
+
}
|
|
347
|
+
|
|
294
348
|
/**
|
|
295
349
|
* Invert the color by complementing each RGB channel.
|
|
296
350
|
*
|
package/src/tools/index.js
CHANGED
|
@@ -20,7 +20,15 @@ export { default as padding } from "./padding.js";
|
|
|
20
20
|
export { print } from "./print.js";
|
|
21
21
|
export { random } from "./random.js";
|
|
22
22
|
export { default as rgba } from "./rgba.js";
|
|
23
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
SPACING_SCALE,
|
|
25
|
+
RADIUS_SCALE,
|
|
26
|
+
TYPE_SCALE,
|
|
27
|
+
TAP_MINIMUM,
|
|
28
|
+
PILL,
|
|
29
|
+
LABEL_TEXT,
|
|
30
|
+
} from "./scale.js";
|
|
31
|
+
export { default as shadow, SHADOW_LEVELS } from "./shadow.js";
|
|
24
32
|
export {
|
|
25
33
|
capitalize,
|
|
26
34
|
capitalizeWords,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// scale.js - The framework's design scales: the numbers every widget default is drawn from.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The clean-UI contract, expressed as data instead of prose.
|
|
5
|
+
*
|
|
6
|
+
* These are the same scales `flet-box check-ui` enforces against an app's own
|
|
7
|
+
* source. They live here because a gate that only reads the app cannot make the
|
|
8
|
+
* app clean: it can reject a `padding: 21`, but the widget defaults the app never
|
|
9
|
+
* typed already decide the rhythm, the radius and the tap size of every screen.
|
|
10
|
+
* A default below the Fitts floor is a rule the framework breaks on the user's
|
|
11
|
+
* behalf, and no amount of app-side linting can undo it.
|
|
12
|
+
*
|
|
13
|
+
* Units are framework units, not CSS pixels. A numeric length prop goes through
|
|
14
|
+
* the factory's `toREM` (`src/utils/units.js`), so `40` is emitted as `2.5rem`:
|
|
15
|
+
* 40px at the 16px base, and *bigger* when the reader raises their browser font.
|
|
16
|
+
* That is the point — a tap target written in rem obeys the accessibility
|
|
17
|
+
* setting, while the 44px the guidelines quote is a fixed-pixel floor that
|
|
18
|
+
* shrinks in relative terms the moment the type grows.
|
|
19
|
+
*
|
|
20
|
+
* Depth is the exception: `SHADOW_LEVELS` ships raw `px` inside the box-shadow
|
|
21
|
+
* string on purpose, because a shadow that scales with the reader's font size
|
|
22
|
+
* stops matching the surface it lifts.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A radius this large clamps to half the box: a pill, whatever the widget's size. */
|
|
26
|
+
export const PILL = 999;
|
|
27
|
+
|
|
28
|
+
/** The 8-based rhythm, with the two steps a real layout needs (18 for field text, 96 for hero gutters). */
|
|
29
|
+
export const SPACING_SCALE = new Set([0, 4, 8, 12, 16, 18, 24, 32, 40, 48, 64, 96]);
|
|
30
|
+
|
|
31
|
+
/** Corner radii. `999` is a pill, `0` a flush edge — both are decisions, not omissions. */
|
|
32
|
+
export const RADIUS_SCALE = new Set([0, 4, 8, 12, 16, PILL]);
|
|
33
|
+
|
|
34
|
+
/** Type sizes. Two steps per jump: a 15 next to a 16 reads as a mistake, not as hierarchy. */
|
|
35
|
+
export const TYPE_SCALE = new Set([12, 14, 16, 18, 24, 32, 40, 48]);
|
|
36
|
+
|
|
37
|
+
/** Fitts: the smallest pressable box a finger reliably hits. */
|
|
38
|
+
export const TAP_MINIMUM = 40;
|
|
39
|
+
|
|
40
|
+
/** The reading size a control's own label may not fall under. */
|
|
41
|
+
export const LABEL_TEXT = 14;
|
package/src/tools/shadow.js
CHANGED
|
@@ -20,4 +20,24 @@
|
|
|
20
20
|
const shadow = (x, y, blur, spread, color) =>
|
|
21
21
|
`${x}px ${y}px ${blur}px ${spread}px ${color}`;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The framework's depth scale, shared by every widget that lifts off the page.
|
|
25
|
+
*
|
|
26
|
+
* The engine routes `elevation` straight into `style.boxShadow`, so a bare
|
|
27
|
+
* number is an invalid declaration the browser drops without a word: a widget
|
|
28
|
+
* that forwards `elevation` instead of one of these strings looks configured and
|
|
29
|
+
* paints flat. A widget that wants a numeric depth reads the level here and
|
|
30
|
+
* passes the resulting string.
|
|
31
|
+
*
|
|
32
|
+
* `0` is deliberately `none` — a flush surface is a decision, not a missing one.
|
|
33
|
+
*/
|
|
34
|
+
export const SHADOW_LEVELS = {
|
|
35
|
+
0: "none",
|
|
36
|
+
1: "0 1px 3px rgba(0,0,0,0.12)",
|
|
37
|
+
2: "0 3px 6px rgba(0,0,0,0.16)",
|
|
38
|
+
3: "0 6px 12px rgba(0,0,0,0.2)",
|
|
39
|
+
4: "0 10px 20px rgba(0,0,0,0.25)",
|
|
40
|
+
5: "0 15px 30px rgba(0,0,0,0.3)",
|
|
41
|
+
};
|
|
42
|
+
|
|
23
43
|
export default shadow;
|
package/src/utils/index.js
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { color } from "../tools/color.js";
|
|
2
|
+
|
|
3
|
+
const SOLID_COLOR =
|
|
4
|
+
/^(#[0-9a-f]{3,8}|rgba?\(\s*\d|hsla?\(\s*\d|black|white|red|green|blue|yellow|cyan|magenta|gray|darkgray|lightgray)$/i;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Black or white, whichever reads better on `background` by WCAG contrast.
|
|
8
|
+
* Falls back when the fill is not a solid color this code can measure (a
|
|
9
|
+
* gradient, a CSS variable) — guessing there is worse than the caller's choice.
|
|
10
|
+
*/
|
|
11
|
+
export const readableInk = (background, fallback) => {
|
|
12
|
+
if (typeof background !== "string" || !SOLID_COLOR.test(background)) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
const bg = color(background);
|
|
16
|
+
return bg.contrastRatio("#ffffff") >= bg.contrastRatio("#000000")
|
|
17
|
+
? "#ffffff"
|
|
18
|
+
: "#000000";
|
|
19
|
+
};
|
|
@@ -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 };
|