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.
Files changed (139) hide show
  1. package/README.md +3 -0
  2. package/bin/cli.js +13 -0
  3. package/bin/commands/checkApi.js +13 -1
  4. package/bin/commands/checkUi.js +1369 -0
  5. package/bin/commands/create.js +49 -5
  6. package/bin/commands/createBundle.js +21 -0
  7. package/bin/commands/runServer.js +11 -2
  8. package/bin/utils/pwaShell.js +137 -0
  9. package/bin/utils/templates.js +1398 -443
  10. package/docs/README.md +4 -0
  11. package/docs/cli/README.md +63 -2
  12. package/docs/guides/README.md +2 -0
  13. package/docs/guides/clean-ui.md +211 -0
  14. package/docs/guides/minification-and-protection.md +1 -1
  15. package/docs/guides/mobile-and-platforms.md +31 -0
  16. package/docs/guides/online-updates.md +48 -0
  17. package/docs/tools/README.md +1 -1
  18. package/docs/tools/animation.md +1 -1
  19. package/docs/tools/markdown.md +3 -3
  20. package/docs/tools/theme.md +42 -1
  21. package/docs/widget/Accordion.md +3 -0
  22. package/docs/widget/AdMob.md +4 -1
  23. package/docs/widget/AdSense.md +3 -0
  24. package/docs/widget/AnimatedBox.md +6 -3
  25. package/docs/widget/AnimatedText.md +3 -0
  26. package/docs/widget/Audio.md +3 -0
  27. package/docs/widget/Avatar.md +3 -0
  28. package/docs/widget/Badge.md +3 -0
  29. package/docs/widget/Button.md +3 -0
  30. package/docs/widget/COMMON_PROPS.md +52 -0
  31. package/docs/widget/Card.md +3 -0
  32. package/docs/widget/Carousel.md +3 -0
  33. package/docs/widget/Chart.md +3 -0
  34. package/docs/widget/Checkbox.md +3 -0
  35. package/docs/widget/Chip.md +3 -0
  36. package/docs/widget/CircularBar.md +3 -0
  37. package/docs/widget/CircularChart.md +3 -0
  38. package/docs/widget/CodeViewer.md +3 -0
  39. package/docs/widget/Column.md +3 -0
  40. package/docs/widget/Container.md +3 -0
  41. package/docs/widget/DataTable.md +3 -0
  42. package/docs/widget/Divider.md +3 -0
  43. package/docs/widget/DraggBox.md +3 -0
  44. package/docs/widget/Dropdown.md +3 -0
  45. package/docs/widget/DroppBox.md +3 -0
  46. package/docs/widget/FloatingActionButton.md +3 -0
  47. package/docs/widget/GridView.md +3 -0
  48. package/docs/widget/Icon.md +3 -0
  49. package/docs/widget/Image.md +3 -0
  50. package/docs/widget/Input.md +3 -0
  51. package/docs/widget/Inspector.md +2 -2
  52. package/docs/widget/InstallButton.md +3 -0
  53. package/docs/widget/ListTile.md +3 -0
  54. package/docs/widget/ListView.md +3 -0
  55. package/docs/widget/Maps.md +3 -0
  56. package/docs/widget/Markdown.md +3 -0
  57. package/docs/widget/MatrixRain.md +3 -0
  58. package/docs/widget/Pagination.md +3 -0
  59. package/docs/widget/ParallaxBox.md +3 -0
  60. package/docs/widget/ProgressBar.md +3 -0
  61. package/docs/widget/QRCode.md +3 -0
  62. package/docs/widget/README.md +2 -0
  63. package/docs/widget/Radio.md +4 -1
  64. package/docs/widget/Rating.md +3 -0
  65. package/docs/widget/Row.md +3 -0
  66. package/docs/widget/Skeleton.md +3 -0
  67. package/docs/widget/Slider.md +3 -0
  68. package/docs/widget/Stack.md +3 -0
  69. package/docs/widget/Stepper.md +3 -0
  70. package/docs/widget/Switch.md +3 -0
  71. package/docs/widget/Tabs.md +5 -5
  72. package/docs/widget/Text.md +3 -0
  73. package/docs/widget/Tooltip.md +3 -0
  74. package/docs/widget/TreeView.md +3 -0
  75. package/docs/widget/Video.md +3 -0
  76. package/package.json +2 -2
  77. package/src/animations/AnimatedBox.js +4 -1
  78. package/src/animations/MatrixRain.js +3 -3
  79. package/src/index.d.ts +64 -8
  80. package/src/index.js +1 -0
  81. package/src/navigations/AppBar.js +7 -12
  82. package/src/navigations/BottomNavigation.js +8 -13
  83. package/src/navigations/CollapsibleSideBar.js +30 -8
  84. package/src/navigations/Drawer.js +16 -14
  85. package/src/navigations/DrawerItem.js +3 -1
  86. package/src/navigations/Scaffold.js +26 -12
  87. package/src/navigations/Tabs.js +104 -59
  88. package/src/tools/color.js +54 -0
  89. package/src/tools/index.js +9 -1
  90. package/src/tools/scale.js +41 -0
  91. package/src/tools/shadow.js +20 -0
  92. package/src/utils/index.js +1 -0
  93. package/src/utils/readableInk.js +19 -0
  94. package/src/utils/syntaxHighlight.js +78 -10
  95. package/src/utils/themes.js +265 -58
  96. package/src/utils/units.js +33 -11
  97. package/src/widget-factory/effects.js +56 -5
  98. package/src/widget-factory/processProps.js +10 -1
  99. package/src/widget-factory/remTool.js +15 -29
  100. package/src/widget-factory/stackPosition.js +18 -3
  101. package/src/widget-factory/translateProps.js +28 -0
  102. package/src/widgets/Accordion.js +20 -23
  103. package/src/widgets/AdMob.js +7 -2
  104. package/src/widgets/Avatar.js +18 -12
  105. package/src/widgets/Badge.js +3 -2
  106. package/src/widgets/BottomSheet.js +14 -13
  107. package/src/widgets/Button.js +69 -30
  108. package/src/widgets/Card.js +6 -2
  109. package/src/widgets/Carousel.js +12 -10
  110. package/src/widgets/Chart.js +28 -5
  111. package/src/widgets/Checkbox.js +76 -18
  112. package/src/widgets/Chip.js +31 -10
  113. package/src/widgets/CircularBar.js +7 -7
  114. package/src/widgets/CircularChart.js +26 -8
  115. package/src/widgets/CodeViewer.js +93 -20
  116. package/src/widgets/DataTable.js +17 -11
  117. package/src/widgets/DraggBox.js +2 -1
  118. package/src/widgets/Dropdown.js +26 -13
  119. package/src/widgets/DroppBox.js +4 -2
  120. package/src/widgets/FloatingActionButton.js +13 -7
  121. package/src/widgets/Icon.js +3 -1
  122. package/src/widgets/Input.js +30 -23
  123. package/src/widgets/Inspector.js +54 -1
  124. package/src/widgets/InstallButton.js +6 -4
  125. package/src/widgets/ListTile.js +4 -3
  126. package/src/widgets/ListView.js +30 -12
  127. package/src/widgets/Markdown.js +37 -26
  128. package/src/widgets/Modal.js +14 -11
  129. package/src/widgets/Pagination.js +16 -11
  130. package/src/widgets/ProgressBar.js +6 -4
  131. package/src/widgets/Radio.js +73 -15
  132. package/src/widgets/Rating.js +24 -6
  133. package/src/widgets/Skeleton.js +31 -31
  134. package/src/widgets/Slider.js +29 -18
  135. package/src/widgets/SnackBar.js +35 -39
  136. package/src/widgets/Stepper.js +13 -9
  137. package/src/widgets/Switch.js +59 -18
  138. package/src/widgets/Tooltip.js +12 -10
  139. package/src/widgets/TreeView.js +27 -15
@@ -39,19 +39,19 @@ Tabs({
39
39
  | `onChange` | function | — | Fired with the new index when the tab changes. |
40
40
  | `variant` | `'underline'`/`'filled'`/`'slider'`/`'pills'` | `'underline'` | Visual treatment of the tab bar. |
41
41
  | `size` | `'small'`/`'medium'`/`'large'` | `'medium'` | Font, padding, icon, and height presets. |
42
- | `textColor` | Color | `colors.text` | Inactive label/icon color. |
43
- | `activeTextColor` | Color | `colors.white` | Active label/icon color. |
42
+ | `textColor` | Color | `colors.textSecondary` | Inactive label/icon color. |
43
+ | `activeTextColor` | Color | ink on `slider`, `colors.text` otherwise | Active label/icon color. |
44
44
  | `bgColor` | Color | `colors.gray100` | Bar background for `slider`/`filled` variants. |
45
- | `buttonColor` | Color | `colors.secondary` | Background of the `slider` indicator and `pills`. |
45
+ | `buttonColor` | Color | `color` | Background of the `slider` indicator. |
46
46
  | `dividerColor` | Color | `colors.border` | Bottom border for the `underline` variant (when `showDivider`). |
47
47
  | `fullWidth` | boolean | `true` | Tabs share the full width; otherwise they hug their content. |
48
48
  | `showDivider` | boolean | `true` | Bottom divider under an `underline` bar. |
49
49
  | `showIcon` | boolean | `false` | Show the per-tab `icon`. |
50
- | `iconPosition` | `'left'`/`'right'`/`'top'`/`'bottom'` | `'left'` | Where the icon sits: `'left'` places it before the label; any other value places it after. |
50
+ | `iconPosition` | `'left'`/`'right'`/`'top'`/`'bottom'` | `'left'` | Where the icon sits: `'left'` before the label, `'right'` after it, `'top'`/`'bottom'` stacked above/below the label row. |
51
51
  | `iconSize` | number | `18` | Icon size in pixels. |
52
52
  | `badges` | array | `[]` | Badge values per tab (`badges[i]`), or set them inline via `{ badge }`. |
53
53
  | `alignment` | `'left'`/`'center'`/`'right'` | `'left'` | Accepted for API compatibility; currently has no visual effect. |
54
- | `color` | Color | `colors.primary` | Accepted for API compatibility; active styling uses `activeTextColor`/`buttonColor` instead. |
54
+ | `color` | Color | `colors.primary` | Accent: the `underline` indicator, the `pills` tint, and the default `buttonColor`. |
55
55
 
56
56
  These are the props specific to `Tabs`. It also accepts every [common prop](COMMON_PROPS.md).
57
57
 
@@ -54,6 +54,9 @@ Every widget also accepts these shared props — see [common props](COMMON_PROPS
54
54
  | `borderRadius` | `number \| string` | Rounds the corners of the widget. |
55
55
  | `elevation` | `number` | Shadow depth. Higher values lift the widget off the page. |
56
56
  | `shadow` | `string` | Raw CSS `box-shadow` value, for a custom shadow. |
57
+ | `filter` | `string` | Raw CSS `filter` value applied to the widget itself, e.g. `blur(4px)`. |
58
+ | `backdropFilter` | `string` | Raw CSS `backdrop-filter` value: what sits behind the widget is blurred through it, which is what makes a translucent card frosted glass. |
59
+ | `maskImage` | `string` | Raw CSS `mask-image` value, used to fade a widget out at its edges instead of cutting it. |
57
60
  | `opacity` | `number` | Opacity from 0 (invisible) to 1 (fully opaque). |
58
61
  | `visible` | `boolean` | Whether the widget is rendered and visible. |
59
62
  | `disabled` | `boolean` | Disables interaction and shows the non-interactive state. |
@@ -62,6 +62,9 @@ Every widget also accepts these shared props — see [common props](COMMON_PROPS
62
62
  | `borderRadius` | `number \| string` | Rounds the corners of the widget. |
63
63
  | `elevation` | `number` | Shadow depth. Higher values lift the widget off the page. |
64
64
  | `shadow` | `string` | Raw CSS `box-shadow` value, for a custom shadow. |
65
+ | `filter` | `string` | Raw CSS `filter` value applied to the widget itself, e.g. `blur(4px)`. |
66
+ | `backdropFilter` | `string` | Raw CSS `backdrop-filter` value: what sits behind the widget is blurred through it, which is what makes a translucent card frosted glass. |
67
+ | `maskImage` | `string` | Raw CSS `mask-image` value, used to fade a widget out at its edges instead of cutting it. |
65
68
  | `opacity` | `number` | Opacity from 0 (invisible) to 1 (fully opaque). |
66
69
  | `visible` | `boolean` | Whether the widget is rendered and visible. |
67
70
  | `disabled` | `boolean` | Disables interaction and shows the non-interactive state. |
@@ -77,6 +77,9 @@ Every widget also accepts these shared props — see [common props](COMMON_PROPS
77
77
  | `borderRadius` | `number \| string` | Rounds the corners of the widget. |
78
78
  | `elevation` | `number` | Shadow depth. Higher values lift the widget off the page. |
79
79
  | `shadow` | `string` | Raw CSS `box-shadow` value, for a custom shadow. |
80
+ | `filter` | `string` | Raw CSS `filter` value applied to the widget itself, e.g. `blur(4px)`. |
81
+ | `backdropFilter` | `string` | Raw CSS `backdrop-filter` value: what sits behind the widget is blurred through it, which is what makes a translucent card frosted glass. |
82
+ | `maskImage` | `string` | Raw CSS `mask-image` value, used to fade a widget out at its edges instead of cutting it. |
80
83
  | `opacity` | `number` | Opacity from 0 (invisible) to 1 (fully opaque). |
81
84
  | `visible` | `boolean` | Whether the widget is rendered and visible. |
82
85
  | `disabled` | `boolean` | Disables interaction and shows the non-interactive state. |
@@ -49,6 +49,9 @@ Every widget also accepts these shared props — see [common props](COMMON_PROPS
49
49
  | `borderRadius` | `number \| string` | Rounds the corners of the widget. |
50
50
  | `elevation` | `number` | Shadow depth. Higher values lift the widget off the page. |
51
51
  | `shadow` | `string` | Raw CSS `box-shadow` value, for a custom shadow. |
52
+ | `filter` | `string` | Raw CSS `filter` value applied to the widget itself, e.g. `blur(4px)`. |
53
+ | `backdropFilter` | `string` | Raw CSS `backdrop-filter` value: what sits behind the widget is blurred through it, which is what makes a translucent card frosted glass. |
54
+ | `maskImage` | `string` | Raw CSS `mask-image` value, used to fade a widget out at its edges instead of cutting it. |
52
55
  | `opacity` | `number` | Opacity from 0 (invisible) to 1 (fully opaque). |
53
56
  | `visible` | `boolean` | Whether the widget is rendered and visible. |
54
57
  | `disabled` | `boolean` | Disables interaction and shows the non-interactive state. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flet-box",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Declarative, zero-dependency UI framework in vanilla JS — ship web, PWA, Android and iOS from one codebase",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -19,7 +19,7 @@
19
19
  "dev": "node bin/cli.js run --hot",
20
20
  "build": "node bin/cli.js createBundle",
21
21
  "release": "node bin/cli.js release",
22
- "test": "node scripts/check-widgets.mjs && node scripts/check-prop-registry.mjs && node scripts/check-docs-props.mjs && node scripts/check-api-surface.mjs && node scripts/check-router-base.mjs && node scripts/check-ota-assets.mjs && node scripts/check-cli-install.mjs && python3 tools/snippets-creator-flet-box/build_snippets.py --check && node scripts/check-snippets.mjs",
22
+ "test": "node scripts/check-widgets.mjs && node scripts/check-widget-scale.mjs && node scripts/check-prop-registry.mjs && node scripts/check-docs-props.mjs && node scripts/check-api-surface.mjs && node scripts/check-api-propscan.mjs && node scripts/check-router-base.mjs && node scripts/check-clean-ui.mjs && node scripts/check-ota-assets.mjs && node scripts/check-pwa-shell.mjs && node scripts/check-cli-install.mjs && python3 tools/snippets-creator-flet-box/build_snippets.py --check && node scripts/check-snippets.mjs",
23
23
  "test:browser": "node scripts/run-browser-tests.mjs"
24
24
  },
25
25
  "keywords": [
@@ -381,7 +381,10 @@ const generateKeyframes = (animations) => {
381
381
 
382
382
  animations.forEach((anim) => {
383
383
  const effect = effectMap[anim.effect] || anim.effect;
384
- const { from, to, reverse = true } = anim;
384
+ // Straight `from → to` is the default: a one-shot entry animation must hold
385
+ // `to`. Cycling back is opt-in, or every documented `opacity 0 → 1` faded
386
+ // to invisible again at 100%.
387
+ const { from, to, reverse = false } = anim;
385
388
 
386
389
  // A descriptor without `from`/`to` is malformed. Skip it with a warning
387
390
  // rather than throwing a TypeError out of the caller's entire render.
@@ -219,8 +219,9 @@ export const MatrixRain = (props = {}) => {
219
219
  // above), because the canvas has no parent until the caller appends it.
220
220
 
221
221
  /**
222
- * Stops the animation loop, removes event listeners, disconnects the
223
- * ResizeObserver, and removes the canvas from the DOM.
222
+ * Stops the animation loop, removes the window listeners and disconnects the
223
+ * ResizeObserver. The canvas stays where it is: the caller owns the element and
224
+ * decides whether to remove it, which is what the module contract documents.
224
225
  *
225
226
  * @returns {void}
226
227
  */
@@ -228,7 +229,6 @@ export const MatrixRain = (props = {}) => {
228
229
  if (animationId) cancelAnimationFrame(animationId);
229
230
  window.removeEventListener("resize", handleResize);
230
231
  if (resizeObserver) resizeObserver.disconnect();
231
- if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
232
232
  };
233
233
 
234
234
  // Chain cleanup so any previously assigned _cleanup is also called
package/src/index.d.ts CHANGED
@@ -30,10 +30,48 @@ declare module 'flet-box' {
30
30
  _cleanup(): void;
31
31
  }
32
32
 
33
+ // =========================================================================
34
+ // Posicionamiento
35
+ // =========================================================================
36
+ /**
37
+ * Positional props accepted by every widget.
38
+ *
39
+ * `position: 'stack'` takes the widget out of normal flow and places it
40
+ * against its nearest positioned ancestor; the `center*` / `full*` shorthands
41
+ * only take effect in that mode. Numeric offsets are framework units (pixels
42
+ * on the 16px base, emitted as rem).
43
+ *
44
+ * A few widgets give `position` their own meaning — the toast/badge/tooltip
45
+ * anchor — and therefore omit this declaration, as does `Maps` for `center`.
46
+ */
47
+ export interface StackPositionProps {
48
+ position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' | 'stack';
49
+ /** Alias of `position`. */
50
+ pos?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky' | 'stack';
51
+ top?: Size;
52
+ right?: Size;
53
+ bottom?: Size;
54
+ left?: Size;
55
+ inset?: Size;
56
+ /** Logical spelling of `left`. */
57
+ start?: Size;
58
+ /** Logical spelling of `right`. */
59
+ end?: Size;
60
+ /** Alias of `zIndex`. */
61
+ z?: number;
62
+ zIndex?: number;
63
+ centerX?: boolean;
64
+ centerY?: boolean;
65
+ center?: boolean;
66
+ fullWidth?: boolean;
67
+ fullHeight?: boolean;
68
+ full?: boolean;
69
+ }
70
+
33
71
  // =========================================================================
34
72
  // Props comunes (heredadas by todos the widgets)
35
73
  // =========================================================================
36
- export interface CommonProps {
74
+ export interface CommonProps extends StackPositionProps {
37
75
  width?: Size;
38
76
  height?: Size;
39
77
  // `Size`, not `number`: for a Text this is a font size and for a Button it is
@@ -49,6 +87,12 @@ declare module 'flet-box' {
49
87
  borderRadius?: number | string;
50
88
  elevation?: number;
51
89
  shadow?: string;
90
+ // Glass and filters. A CSS value string, not an object: `backdropFilter` is
91
+ // what makes a translucent card frosted rather than merely see-through, and
92
+ // `maskImage` is what fades a list out at its end.
93
+ filter?: string;
94
+ backdropFilter?: string;
95
+ maskImage?: string;
52
96
  opacity?: number;
53
97
  visible?: boolean;
54
98
  disabled?: boolean;
@@ -242,7 +286,7 @@ declare module 'flet-box' {
242
286
 
243
287
  export interface StackProps extends CommonProps {
244
288
  children?: Widget[];
245
- position?: 'relative' | 'absolute';
289
+ position?: 'relative' | 'absolute' | 'stack';
246
290
  alignment?: 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
247
291
  }
248
292
  export function Stack(props: StackProps): Widget;
@@ -489,7 +533,7 @@ declare module 'flet-box' {
489
533
  getElement(): Widget;
490
534
  };
491
535
 
492
- export interface AdMobProps extends CommonProps {
536
+ export interface AdMobProps extends Omit<CommonProps, 'position'> {
493
537
  adId?: string;
494
538
  isTesting?: boolean;
495
539
  position?: 'top' | 'bottom';
@@ -520,7 +564,7 @@ declare module 'flet-box' {
520
564
  getPlugin(): object | null;
521
565
  };
522
566
 
523
- export interface BadgeProps extends CommonProps {
567
+ export interface BadgeProps extends Omit<CommonProps, 'position'> {
524
568
  value: string | number;
525
569
  child: Widget;
526
570
  bgColor?: Color;
@@ -1282,9 +1326,14 @@ declare module 'flet-box' {
1282
1326
  };
1283
1327
 
1284
1328
  export interface InspectorProps {
1329
+ /** The widget whose call is reconstructed and painted. */
1285
1330
  widget: Widget;
1331
+ /** Starting indentation depth of the printout. */
1286
1332
  indent?: number;
1287
1333
  }
1334
+ /** Paints the reconstructed call of a widget tree. */
1335
+ export function Inspector(props: InspectorProps): Widget;
1336
+ /** Rebuilds the call that made a widget, as a string. */
1288
1337
  export function Inspector(widget: Widget, indent?: number): string;
1289
1338
  export function printWidgetCode(widget: Widget): void;
1290
1339
  export function inspectWidget(widget: Widget): Widget;
@@ -1362,7 +1411,7 @@ declare module 'flet-box' {
1362
1411
  getCurrentIndex(): number;
1363
1412
  };
1364
1413
 
1365
- export interface TooltipProps extends CommonProps {
1414
+ export interface TooltipProps extends Omit<CommonProps, 'position'> {
1366
1415
  text: string;
1367
1416
  child: Widget;
1368
1417
  position?: 'top' | 'bottom' | 'left' | 'right';
@@ -1770,7 +1819,7 @@ declare module 'flet-box' {
1770
1819
  heading: number | null;
1771
1820
  }
1772
1821
 
1773
- export interface MapsProps extends CommonProps {
1822
+ export interface MapsProps extends Omit<CommonProps, 'center'> {
1774
1823
  /** Initial view centre. Defaults to `[20, 0]`. */
1775
1824
  center?: LatLng;
1776
1825
  /** Integer zoom level, 0 = the whole world. Defaults to `2`. */
@@ -1884,6 +1933,10 @@ declare module 'flet-box' {
1884
1933
  export const colors: Record<string, string>;
1885
1934
  export const palettes: Record<string, Record<string, string>>;
1886
1935
  export function setTheme(themeName: 'light' | 'dark'): Record<string, string>;
1936
+ export function registerPalette(
1937
+ themeName: string,
1938
+ palette: Record<string, string>
1939
+ ): Record<string, string>;
1887
1940
  export function getTheme(): string;
1888
1941
  export function toggleTheme(): string;
1889
1942
  export function getColor(colorName: string, opacity?: number): string;
@@ -2084,9 +2137,12 @@ declare module 'flet-box' {
2084
2137
  isDark(): boolean;
2085
2138
  isLight(): boolean;
2086
2139
  contrast(): string;
2140
+ luminance(): number;
2141
+ contrastRatio(other: string | ColorInstance): number;
2142
+ meetsContrast(other: string | ColorInstance, minimum?: number): boolean;
2087
2143
  }
2088
2144
 
2089
- export const color: (value: string) => ColorInstance;
2145
+ export const color: (value: string | ColorInstance) => ColorInstance;
2090
2146
  export function rgba(r: number, g: number, b: number, a?: number): string;
2091
2147
  export function stackPosition(widget: Widget, props: { top?: any; right?: any; bottom?: any; left?: any }): Widget;
2092
2148
  export function flex(grow: number | { grow?: number; shrink?: number; basis?: string }, shrink?: number, basis?: string): string;
@@ -2195,5 +2251,5 @@ declare module 'flet-box' {
2195
2251
  // =========================================================================
2196
2252
  export const highlightColors: Record<string, string>;
2197
2253
  export function tokenize(code: string): Array<{ type: string; value: string }>;
2198
- export function generateHighlightedHtml(code: string): string;
2254
+ export function generateHighlightedHtml(code: string, palette?: Record<string, string>): string;
2199
2255
  }
package/src/index.js CHANGED
@@ -87,6 +87,7 @@ export {
87
87
  export {
88
88
  colors,
89
89
  setTheme,
90
+ registerPalette,
90
91
  getTheme,
91
92
  toggleTheme,
92
93
  subscribeTheme,
@@ -59,6 +59,9 @@ import { goBack, goTo } from "./Router.js";
59
59
  * - `setBackgroundColor(color: string)` — swap the background color or gradient.
60
60
  * - `show()` / `hide()` — toggle bar visibility.
61
61
  */
62
+ import { toREM } from "../utils/units.js";
63
+ import { SHADOW_LEVELS } from "../tools/shadow.js";
64
+
62
65
  export const AppBar = (props) => {
63
66
  const {
64
67
  title,
@@ -119,18 +122,10 @@ export const AppBar = (props) => {
119
122
  const finalPaddingRight = paddingRight || padding;
120
123
 
121
124
  const finalBorderRadius =
122
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
125
+ toREM(borderRadius);
123
126
 
124
- // Material Design elevation → box-shadow lookup table.
125
- const shadows = {
126
- 0: "none",
127
- 1: "0 1px 3px rgba(0,0,0,0.12)",
128
- 2: "0 3px 6px rgba(0,0,0,0.16)",
129
- 3: "0 6px 12px rgba(0,0,0,0.2)",
130
- 4: "0 10px 20px rgba(0,0,0,0.25)",
131
- 5: "0 15px 30px rgba(0,0,0,0.3)",
132
- };
133
- const boxShadow = shadow === true ? shadows[elevation] : shadow || "none";
127
+ // The framework's depth scale, not a local copy of it (src/tools/shadow.js).
128
+ const boxShadow = shadow === true ? SHADOW_LEVELS[elevation] : shadow || "none";
134
129
 
135
130
  // ========== LEADING SECTION ==========
136
131
  // Auto-create a back button if requested and no custom leading was supplied.
@@ -173,7 +168,7 @@ export const AppBar = (props) => {
173
168
  marginBottom: finalMarginBottom,
174
169
  marginLeft: finalMarginLeft,
175
170
  marginRight: finalMarginRight,
176
- paddingTop: `calc(${finalPaddingTop}px + env(safe-area-inset-top, 0px))`,
171
+ paddingTop: `calc(${toREM(finalPaddingTop)} + env(safe-area-inset-top, 0px))`,
177
172
  paddingBottom: finalPaddingBottom,
178
173
  paddingLeft: finalPaddingLeft,
179
174
  paddingRight: finalPaddingRight,
@@ -53,6 +53,9 @@ import { getCurrentPath, subscribe, goTo } from "./Router.js";
53
53
  * - `getActiveIndex(): number` — returns the currently active tab index.
54
54
  * - `getCurrentRoute(): string|null` — returns the current router path (router mode only).
55
55
  */
56
+ import { toREM } from "../utils/units.js";
57
+ import { SHADOW_LEVELS } from "../tools/shadow.js";
58
+
56
59
  export const BottomNavigation = (props) => {
57
60
  const {
58
61
  items = [],
@@ -90,24 +93,16 @@ export const BottomNavigation = (props) => {
90
93
  const finalMarginLeft = marginLeft || margin;
91
94
  const finalMarginRight = marginRight || margin;
92
95
  const finalBorderRadius =
93
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
94
- const finalPadding = typeof padding === "number" ? `${padding}px` : padding;
96
+ toREM(borderRadius);
97
+ const finalPadding = toREM(padding);
95
98
 
96
- // Material Design elevation → box-shadow lookup table.
97
- const shadows = {
98
- 0: "none",
99
- 1: "0 1px 3px rgba(0,0,0,0.12)",
100
- 2: "0 3px 6px rgba(0,0,0,0.16)",
101
- 3: "0 6px 12px rgba(0,0,0,0.2)",
102
- 4: "0 10px 20px rgba(0,0,0,0.25)",
103
- 5: "0 15px 30px rgba(0,0,0,0.3)",
104
- };
105
- const boxShadow = shadow === true ? shadows[elevation] : shadow || "none";
99
+ // The framework's depth scale, not a local copy of it (src/tools/shadow.js).
100
+ const boxShadow = shadow === true ? SHADOW_LEVELS[elevation] : shadow || "none";
106
101
 
107
102
  const container = WidgetFactory({
108
103
  tag: "nav",
109
104
  display: "flex",
110
- width: `calc(100% - ${finalMarginLeft}px - ${finalMarginRight}px)`,
105
+ width: `calc(100% - ${toREM(finalMarginLeft)} - ${toREM(finalMarginRight)})`,
111
106
  height: height,
112
107
  backgroundColor: backgroundColor,
113
108
  borderRadius: finalBorderRadius,
@@ -42,6 +42,8 @@ import { colors } from "../utils/themes.js";
42
42
  * @returns {HTMLElement} The sidebar container element, augmented with:
43
43
  * - `updateWidth(w: number|string)` — resize the sidebar programmatically.
44
44
  */
45
+ import { toREM } from "../utils/units.js";
46
+
45
47
  export const CollapsibleSideBar = ({
46
48
  children,
47
49
  expanded = true,
@@ -63,14 +65,33 @@ export const CollapsibleSideBar = ({
63
65
  );
64
66
  let expandedState = Boolean(stateValue.valueOf());
65
67
 
68
+ /**
69
+ * Paint the expanded/collapsed state onto the elements built below.
70
+ *
71
+ * The toggle has to do more than record the state: the width, the chevron
72
+ * glyph and the content's interactivity are all written once at build time,
73
+ * so without this the bar never moved.
74
+ *
75
+ * @param {boolean} isExpanded
76
+ * @returns {void}
77
+ */
78
+ const paint = (isExpanded) => {
79
+ container.style.width = toREM(isExpanded ? widthExpanded : widthCollapsed);
80
+ chevron.textContent = isExpanded ? "chevron_left" : "chevron_right";
81
+ contentWrapper.style.opacity = isExpanded ? "1" : "0";
82
+ contentWrapper.style.pointerEvents = isExpanded ? "auto" : "none";
83
+ };
84
+
66
85
  /**
67
86
  * Toggle the sidebar between expanded and collapsed states.
68
- * Updates the persisted state and fires the `onToggle` callback.
87
+ * Updates the persisted state, repaints the bar and fires the `onToggle`
88
+ * callback.
69
89
  */
70
90
  const toggle = () => {
71
91
  const newState = !expandedState;
72
92
  expandedState = newState;
73
93
  setIsExpanded(newState);
94
+ paint(newState);
74
95
  if (onToggle) onToggle(newState);
75
96
  };
76
97
 
@@ -81,7 +102,7 @@ export const CollapsibleSideBar = ({
81
102
  tag: "div",
82
103
  id,
83
104
  width:
84
- typeof currentWidth === "number" ? `${currentWidth}px` : currentWidth,
105
+ toREM(currentWidth),
85
106
  height: "100%",
86
107
  display: "flex",
87
108
  flexDirection: "column",
@@ -95,15 +116,16 @@ export const CollapsibleSideBar = ({
95
116
 
96
117
  // Toggle button — always visible so the user can expand/collapse regardless
97
118
  // of the current state. The icon flips direction to reflect the state.
119
+ const chevron = Icon({
120
+ name: expandedState ? "chevron_left" : "chevron_right",
121
+ size: iconSize,
122
+ color: colors.textSecondary,
123
+ });
98
124
  const toggleButton = Container({
99
125
  padding: 16,
100
126
  cursor: "pointer",
101
127
  onclick: toggle,
102
- child: Icon({
103
- name: expandedState ? "chevron_left" : "chevron_right",
104
- size: iconSize,
105
- color: colors.textSecondary,
106
- }),
128
+ child: chevron,
107
129
  });
108
130
  container.appendChild(toggleButton);
109
131
 
@@ -130,7 +152,7 @@ export const CollapsibleSideBar = ({
130
152
  */
131
153
  container.updateWidth = (newWidth) => {
132
154
  container.style.width =
133
- typeof newWidth === "number" ? `${newWidth}px` : newWidth;
155
+ toREM(newWidth);
134
156
  };
135
157
 
136
158
  return container;
@@ -58,6 +58,8 @@ let drawerInstance = null;
58
58
  * @param {DrawerProps} [props={}]
59
59
  * @returns {DrawerInstance}
60
60
  */
61
+ import { toREM } from "../utils/units.js";
62
+
61
63
  export const Drawer = (props = {}) => {
62
64
  const {
63
65
  header,
@@ -107,9 +109,9 @@ export const Drawer = (props = {}) => {
107
109
  // Sliding drawer panel.
108
110
  const drawerPanel = document.createElement("div");
109
111
  drawerPanel.style.position = "absolute";
110
- drawerPanel.style.top = `${margin}px`;
111
- drawerPanel.style.bottom = `${margin}px`;
112
- drawerPanel.style.height = `calc(100% - ${margin * 2}px)`;
112
+ drawerPanel.style.top = toREM(margin);
113
+ drawerPanel.style.bottom = toREM(margin);
114
+ drawerPanel.style.height = `calc(100% - ${toREM(margin * 2)})`;
113
115
  drawerPanel.style.display = "flex";
114
116
  drawerPanel.style.flexDirection = "column";
115
117
  drawerPanel.style.backgroundColor = bgColor;
@@ -118,22 +120,22 @@ export const Drawer = (props = {}) => {
118
120
  // Apply border radius only to the corners that face away from the screen edge.
119
121
  // A left drawer gets rounded right corners; a right drawer gets rounded left corners.
120
122
  if (position === "left") {
121
- drawerPanel.style.left = `${margin}px`;
123
+ drawerPanel.style.left = toREM(margin);
122
124
  drawerPanel.style.right = "auto";
123
- drawerPanel.style.borderTopRightRadius = `${borderRadius}px`;
124
- drawerPanel.style.borderBottomRightRadius = `${borderRadius}px`;
125
+ drawerPanel.style.borderTopRightRadius = toREM(borderRadius);
126
+ drawerPanel.style.borderBottomRightRadius = toREM(borderRadius);
125
127
  drawerPanel.style.borderTopLeftRadius = "0";
126
128
  drawerPanel.style.borderBottomLeftRadius = "0";
127
129
  } else {
128
- drawerPanel.style.right = `${margin}px`;
130
+ drawerPanel.style.right = toREM(margin);
129
131
  drawerPanel.style.left = "auto";
130
- drawerPanel.style.borderTopLeftRadius = `${borderRadius}px`;
131
- drawerPanel.style.borderBottomLeftRadius = `${borderRadius}px`;
132
+ drawerPanel.style.borderTopLeftRadius = toREM(borderRadius);
133
+ drawerPanel.style.borderBottomLeftRadius = toREM(borderRadius);
132
134
  drawerPanel.style.borderTopRightRadius = "0";
133
135
  drawerPanel.style.borderBottomRightRadius = "0";
134
136
  }
135
137
 
136
- drawerPanel.style.width = typeof width === "number" ? `${width}px` : width;
138
+ drawerPanel.style.width = toREM(width);
137
139
  // Start the panel off-screen so the entrance animation slides it in.
138
140
  drawerPanel.style.transform =
139
141
  position === "left" ? "translateX(-100%)" : "translateX(100%)";
@@ -160,8 +162,8 @@ export const Drawer = (props = {}) => {
160
162
  header &&
161
163
  Container({
162
164
  flexShrink: 0,
163
- borderTopLeftRadius: position === "right" ? `${borderRadius}px` : "0",
164
- borderTopRightRadius: position === "left" ? `${borderRadius}px` : "0",
165
+ borderTopLeftRadius: position === "right" ? toREM(borderRadius) : "0",
166
+ borderTopRightRadius: position === "left" ? toREM(borderRadius) : "0",
165
167
  overflow: "hidden",
166
168
  child: header,
167
169
  }),
@@ -188,9 +190,9 @@ export const Drawer = (props = {}) => {
188
190
  Container({
189
191
  flexShrink: 0,
190
192
  borderBottomLeftRadius:
191
- position === "right" ? `${borderRadius}px` : "0",
193
+ position === "right" ? toREM(borderRadius) : "0",
192
194
  borderBottomRightRadius:
193
- position === "left" ? `${borderRadius}px` : "0",
195
+ position === "left" ? toREM(borderRadius) : "0",
194
196
  overflow: "hidden",
195
197
  child: footer,
196
198
  }),
@@ -143,6 +143,8 @@ export const DrawerItem = (props) => {
143
143
  * Order of operations: mark selected → fire callbacks → navigate → close drawer.
144
144
  */
145
145
  const handleClick = () => {
146
+ // `onSelect` marks the transition into the selected state — it is what an
147
+ // app counts to open a section once. `onPress` fires on every tap.
146
148
  if (!isSelected) {
147
149
  isSelected = true;
148
150
  updateUI();
@@ -150,8 +152,8 @@ export const DrawerItem = (props) => {
150
152
  selectionEvent.dispatchEvent(
151
153
  new CustomEvent("drawer-item-selected", { detail: { id: label } }),
152
154
  );
155
+ if (onSelect) onSelect();
153
156
  }
154
- if (onSelect) onSelect();
155
157
  if (onPress) onPress();
156
158
  if (route) goTo(route);
157
159
  if (closeOnPress) closeDrawer();
@@ -40,6 +40,8 @@ import { initRouter, getCurrentRoute, subscribe } from "./Router.js";
40
40
  * - `updateLeftNavBar(cfg)` / `updateRightNavBar(cfg)` — replace sidebar content.
41
41
  * - `setLeftNavBarWidth(w)` / `setRightNavBarWidth(w)` — resize a sidebar at runtime.
42
42
  */
43
+ import { toREM } from "../utils/units.js";
44
+
43
45
  export const Scaffold = (props) => {
44
46
  const {
45
47
  appBar: originalAppBar,
@@ -106,11 +108,15 @@ export const Scaffold = (props) => {
106
108
  * Normalises a prop value to an HTMLElement.
107
109
  * Accepts an HTMLElement directly, a factory function, or null/undefined.
108
110
  *
109
- * @param {HTMLElement|Function|null} item
111
+ * A boolean is the documented "show / hide this slot" value, never a widget,
112
+ * so it resolves to an empty slot instead of reaching appendChild() and
113
+ * throwing on a non-Node.
114
+ *
115
+ * @param {HTMLElement|Function|boolean|null} item
110
116
  * @returns {HTMLElement|null}
111
117
  */
112
118
  const toElement = (item) => {
113
- if (!item) return null;
119
+ if (!item || typeof item === "boolean") return null;
114
120
  if (item instanceof HTMLElement) return item;
115
121
  if (typeof item === "function") return item();
116
122
  return item;
@@ -283,7 +289,7 @@ export const Scaffold = (props) => {
283
289
  if (widget) {
284
290
  leftNavBarContainer.appendChild(widget);
285
291
  if (mainContentContainer)
286
- mainContentContainer.style.width = `calc(100% - ${leftNavBarWidth}px)`;
292
+ mainContentContainer.style.width = `calc(100% - ${toREM(leftNavBarWidth)})`;
287
293
  }
288
294
  };
289
295
 
@@ -359,9 +365,17 @@ export const Scaffold = (props) => {
359
365
  if (closeDrawerOnNavigate && drawerInstance && drawerInstance.close) {
360
366
  drawerInstance.close();
361
367
  }
362
- } else if (body) {
363
- const bodyWidget = makeFullSize(toElement(body));
364
- if (bodyWidget) mainContentContainer.appendChild(bodyWidget);
368
+ } else {
369
+ if (body) {
370
+ const bodyWidget = makeFullSize(toElement(body));
371
+ if (bodyWidget) mainContentContainer.appendChild(bodyWidget);
372
+ }
373
+ // Static mode mounts the shell zones too. They used to be router-only, so
374
+ // the documented `Scaffold({ appBar, body })` example rendered the body
375
+ // with no top bar, bottom bar or FAB at all.
376
+ updateAppBar(originalAppBar !== false);
377
+ updateBottomBar(originalBottomBar !== false);
378
+ updateFab(originalFab !== false);
365
379
  }
366
380
  };
367
381
 
@@ -389,7 +403,7 @@ export const Scaffold = (props) => {
389
403
 
390
404
  if (finalLeftNavBar) {
391
405
  leftNavBarContainer = document.createElement("div");
392
- leftNavBarContainer.style.width = `${leftNavBarWidth}px`;
406
+ leftNavBarContainer.style.width = toREM(leftNavBarWidth);
393
407
  leftNavBarContainer.style.flexShrink = "0";
394
408
  leftNavBarContainer.style.overflow = "auto";
395
409
  leftNavBarContainer.style.height = "100%";
@@ -409,7 +423,7 @@ export const Scaffold = (props) => {
409
423
 
410
424
  if (finalRightNavBar) {
411
425
  rightNavBarContainer = document.createElement("div");
412
- rightNavBarContainer.style.width = `${rightNavBarWidth}px`;
426
+ rightNavBarContainer.style.width = toREM(rightNavBarWidth);
413
427
  rightNavBarContainer.style.flexShrink = "0";
414
428
  rightNavBarContainer.style.overflow = "auto";
415
429
  rightNavBarContainer.style.height = "100%";
@@ -425,8 +439,8 @@ export const Scaffold = (props) => {
425
439
  // FAB slot — absolutely positioned so it floats above content.
426
440
  fabContainer = document.createElement("div");
427
441
  fabContainer.style.position = "absolute";
428
- fabContainer.style.bottom = "16px";
429
- fabContainer.style.right = "16px";
442
+ fabContainer.style.bottom = toREM(16);
443
+ fabContainer.style.right = toREM(16);
430
444
  fabContainer.style.zIndex = "10";
431
445
  container.appendChild(fabContainer);
432
446
 
@@ -528,7 +542,7 @@ export const Scaffold = (props) => {
528
542
  */
529
543
  container.setLeftNavBarWidth = (w) => {
530
544
  if (leftNavBarContainer)
531
- leftNavBarContainer.style.width = typeof w === "number" ? `${w}px` : w;
545
+ leftNavBarContainer.style.width = toREM(w);
532
546
  };
533
547
 
534
548
  /**
@@ -538,7 +552,7 @@ export const Scaffold = (props) => {
538
552
  */
539
553
  container.setRightNavBarWidth = (w) => {
540
554
  if (rightNavBarContainer)
541
- rightNavBarContainer.style.width = typeof w === "number" ? `${w}px` : w;
555
+ rightNavBarContainer.style.width = toREM(w);
542
556
  };
543
557
 
544
558
  return container;